Last time, we discussed the ideas behind choosing between monorepo and multirepo. It is one of the most important decisions when starting a new project or refactoring a current codebase. When we know how to keep our code, we must consider further aspects of the project’s development and future maintenance. Among the most critical parts is the discussion regarding dependencies and their versions.
Dependency versions
We can distinguish two major approaches with a plethora of variations in between. On the one hand, we can share all dependency packages’ versions across all applications, microservices, and builds involved in the project. As a result, we have a single common source of truth, with every package always having the same version wherever it is used. On the other hand, we can separate the versioning, so that every part of the project, with a distinct build, has its own list and may use different versions of dependency packages.
First of all, it is not strictly related to the monorepo vs. multirepo discussion. Of course, it would be easier to share common dependencies when using a single repository. However, it is also possible to enforce that in a multi-repository project. Thus, today’s question is whether it is more profitable to keep all elements dependent on the same packages’ versions, or rather, it is not worth bothering about.
Common versions
Let’s focus on the advantages and disadvantages of the first approach described above. What can we gain and what can we lose when we decide to keep all the elements of our vast project in line? Well, the most important thing is that if all the code relies on identical versions of dependencies, then it is easy to reuse it. That is because the coherent technology stack makes everything we write cooperate just fine. In addition, when we do not have to worry about incompatibilities, everyone in the team is on the same page and ready to jump into any part of the implementation. All in all, this kind of setup makes the project more predictable and allows for more accurate estimations.
On the other hand, there is a considerable inconvenience regarding this path. If we want to be true to our single version resolution, whenever we decide to upgrade a single package used by many services, we need to adjust the implementation in all of them. Obviously, that has to happen only when the dependency in question introduces some breaking changes. Still, there are situations when multiple packages have to be updated simultaneously, and that may require a tremendous effort to accomplish.
That is definitely a drawback, but we can change the approach to a less strict one and spread out the endeavor in time. For instance, we may relax our restrictions for the time being and allow two versions of a single package. Then we update all apps and microservices step by step until the old version is no longer referenced. Finally, we end up with a common dependency again, and that’s what we really wanted.
Separated versions
The opposite approach is to have individual versions per build. Having that, we can surely appreciate the freedom when creating a new component of the project. As a result, there are almost no boundaries to using fresh, brand new packages with the latest features and bug fixes. The same is true when we decide to upgrade the dependencies of a single service. All we need to adjust is the code of the given build without any concern about other parts of the system.
That path is especially beneficial when our project encompasses multiple elements that are not evenly developed. We can focus all our effort on the essential ones and leave others as they are. In certain circumstances, that single thing may notably reduce the maintenance cost.
Now, let’s consider the flaws of the concept of separated versions. There is one quite obvious weakness that comes to mind. When we decide to use a new feature or share a common code between services, they may be on different levels of packages’ versions. Sometimes that is not an issue, and the code works just fine. Nevertheless, on other occasions, we may be forced to adjust their dependencies before proceeding as intended.
The problem, however, is not the time spent on the upgrade itself; sooner or later we will do it anyway. The issue is that we may not be aware that specific tasks require such maintenance before we do the actual work. Eventually, we may fall into the trap of incorrectly estimating assignments because inconsistencies can be concealed until too late.
Conclusion
Common and separated versioning is a fundamental decision regarding our projects. On the one hand, we bind ourselves to the idea of keeping all the code up to date and always using the same packages. It may be cumbersome, but it definitely helps in the long run. On the other hand, we are free to go and every build is independent. That is very tempting; still, we need to consider whether ending up with such inconsistencies in the project is worth the risk.
There is no definitive answer to whether we should have common versions or separated ones. Every project is different, and it is for you to decide how much freedom and restrictions, regarding dependencies, you actually need. It is worth thinking about because there are many partial solutions in between, hence every team may decide on that independently for every project. Cheers!