Avoiding Dependency Hell
A common library dependency pattern that works well for medium sized teams.
This article describes how we get into dependency hell and how using a composite uber commons library improves things. One aspect of dependency hell is where making a dependency change results in a conflict in transitive dependencies. Sometimes the build system will choose the closest or latest artefact and problems will not be apparent as the difference between the two versions is not breaking. Where the difference is a ‘breaking change’, compilation, testing or worst production will fail. In this article we assume every change is breaking. This article is most relevant to platforms where multiple versions of the same artefact cannot co-exist in the same runtime (such as the java JVM). There is a legend in the appendix.
We have a deployable application A, that depends on j and k which in turn depends on d. There is only one version of everything and all projects live in their own VCS.
Here we have an a new version of j added to the system that depends on a new version of d. As application A does not use the new version of j everything is still working fine.
The application A developer now wants to use the latest version of j as it has some great performance improvements. However we now have a problem as j-2 depends on d-2 and k-1 depends on d-1 but only one version of d can be used.
In order to take the latest version of j the developer needs to create a new version of k that also depends on the latest d version.
This is problematic for the developer as:
- they understand the API of k but not its implementation.
- they are not aware of d or what it does let alone the changes required to port from d-1 to d-2.
- what they, and their boss, thought was going to be a quick performance improvement has become a time consuming task
Enter the Uber Commons
Here we have application A dependent on a composite commons project containing sub-projects k, j and d all of which are maintained in the same VCS.
Here performance improvements have been made to the d library for the benefit of j. However as k is also using d changes also need to be made there to ensure compilation. As the developer porting j over to the new version of d understands those changes he is well placed, and forced, to also port k over to the new d version. Application A is still referencing version 1 of the commons suite and continues to work fine.
As before the application A developer now want to use the latest and greatest version of j as it has some great performance improvements. As the API of k or j has not changes they can simply reference the new commons version.
- An uber commons also makes refactoring simpler as the IDE has wider visibility of dependencies.
- An uber commons does not mean as much code as possible should be moved into the commons project. Non shared code used for services or applications should live with the application/service.
- When publishing uber commons all sub project artefacts are published. This will consume more disk space but will also make old artefact cleanup easier as, over time, there will be fewer old artefacts referenced.
- Making the changes required to all dependent commons projects when changing a referenced project keeps the project as a whole more up-to-date and as-such reduced technical debt.
Using an uber commons can improve the maintenance and dependency management of multi project systems making change easier and reducing technical debt.
I use the following conventions in diagrams.