Nicolò Andronio

Nicolò Andronio

Full-stack developer, computer scientist, engineer
Evil Genius in the spare time

Developing in a team: shared environments

Part of the personal growth as a software engineer involves learning to work in a team. This is a natural step in anyone’s career which extends to pretty much any other craft. On the other hand, our profession offers such a vast degree of technical enablement that we are flooded with programmatic tools to automate most tasks, down to the tiniest detail. Despite the rich variety of our ecosystem, however, there are still some areas that need improvement. For example, architecture as a code has only been emerging in the past few years and it’s not uncommon to still rely on haphazard shell scripts glued together by magic. In these areas, software development still needs to grow. Today, I want to focus on something that isn’t often perceived as code: development environments.

What is a development environment?

With this term, I collectively refer to all tools, programs, scripts and utilities that a developer uses on a daily basis in their job. Some examples include:

One development environment to rule them all

Within a team, developers have different backgrounds, experience level, preferences and opinions. It naturally follows that each person is more comfortable with a different set of tools. This is perfectly understandable, but it may rapidly evolve into a blocker for day-to-day activities. Like with standards, the problem is that no one solution is widely accepted.

This leads to fragmentation, which in turn leads to an explosive increase in complexity. Just image a team where:

It’s trivial to notice that, whereas each development environment serves essentially the same purpose, no two configurations are the same. Alice, Bob and Carl will write slightly different code that caters to their own specific setup. Fine details that are specific to one framework or another, one runtime or another, are extremely hard to notice and may cause subtle bugs.

As software engineers, we know very well that our local environment will never be perfectly equivalent to a production environment and we learn to live with that. This discrepancy is enough to keep us up late at night to fix all kinds of odd behaviours. Adding inconsistencies caused by different development setups to the mixture is like begging for trouble. As long as there are variables that we can control, we should take the opportunity to do so. That’s the eternal quest of complexity management.

For the reasons mentioned above, encouraging a consistent development environment across all members of a team contributes to lowering complexity, fosters code cohesion and reduces time spent on troubleshooting.

Development environment as a code

The paradigm of “X as a code” has been a popular trend in the past years. Personally, I think the more we can control automatically the better. Relying on manual setup makes us prone to more trivial mistakes and while code isn’t necessarily bug-proof, at least it’s reproducible and steadfast. That’s why translating a development environment into code is good practice to manage team projects. Here’s several different ways to achieve a higher level of automation.

Write a setup script

Many teams at Mind Foundry share this practice, which is especially useful for bringing new hires up to speed with our current development process. We maintain several setup scripts, one for each project. The scripts can be executed on supported systems and will install all relevant tools we expect developers to use in the day-to-day development cycle, with a very strict policy on accepted versions. On top of that, they will provide all necessary configurations for those tools to run consistently. At the moment, we maintain scripts that can be run on either linux or windows, with the latter requiring more manual approaches in some cases.


Some of the perks we get:

Write and maintain run configurations for your IDE

This is a habit I particularly like. Some IDEs like IntelliJ IDEA or VSCode allow people to share run configurations, i.e. declarative configuration files that can be interpreted by the IDE itself to allow execution of common tasks. Some examples:

To exemplify even further, this is my setup for a project I am working on.


The advantage of sharing run configurations is that it allows all developers to run the same commands on equal ground even if they are not prone to tinkering with scripts and CLI details. Each member of the team should collaborate to improve automation by pitching in those areas they are more expert with.

.env files are nice: use them

.env files simply declare a list of variables that will be loaded in the current environment. They are especially useful at development time as they contain a self-sufficient and consistent set of values that guarantee a component is properly configured. However, versioning .env files is generally discouraged, as it opens up potential security flaws. That’s why it is often better to instead commit a script that is able to generate .env files, so that all developers running that script have the same setup, yet specific credentials like passwords and usernames may differ.


For example, one of our backends is written in TypeScript. The shell script above generates a random password that is then used to spin up a docker container with a local database. The same password is also inserted in the .env file for the main application and in the one tailored for our ORM. All .env files are slightly different since they use different variable names, but their values are cohesively orchestrated by a single conductor. Everything runs automatically by pressing a button.

Of course, this is one approach to enforcing cohesive environments. Other people may prefer to have a set of docker containers coordinated by docker-compose or a local k8s cluster. We feel that, for our local workflow, this is an overkill, but other teams with a richer set of microservices may choose to do just that.

Pin dependency versions

Pinning a version means requiring the exact literal version of a package or module, rather than a set of possible versions. Many package or dependency managers use simple notations to designate ranges of valid versions:

However, I cannot stress this enough: prefer pinning dependency versions whenever possible. Even if not intended, minor changes or patches may introduce differences that change the behaviour of code. It is often challenging to find bugs caused by such changes because their effect slips under the radar very easily due to automatic dependency resolution.