“Software dependency management” just sounds so glam, doesn’t it? If we rephrase it to say “do less, get more” it starts to sparkle a little. Let’s take a look at this essential topic and some rules that Go Tripod follow in our projects.
Except it isn’t. It’s telling that searching for the title of this post on Google turns up the Wikipedia page for “dependency hell“. Adding dependencies is one thing but we’re talking software dependency management here, and that involves not only keeping them up to date, but making sure you actually need those dependencies you’re adding.
Rule 1 of software dependency management: do you need it?
Let’s talk about that last point first. Here we go, we’re adding a dependency:
npm install stringconcat
const joinedString = 'my' + 'string';
This is a contrived example, but back in 2016 a very simply library called left-pad was removed from NPM and caused a great deal of consternation.
So before you run that command willy-nilly, think: do you need it? Or can you just write it yourself?
Rule 2: is it any good?
Before adding the dependency, you need to make sure if it’s of sufficient quality. This doesn’t just mean that it has the features you need, or that you need to do an in-depth source code analysis, but you do need to perform some due diligence:
How popular is your dependency? Are there more popular alternatives?
Popularity doesn’t guarantee quality, but it’s a good indicator that the community relies on it. That should mean that people are willing to maintain it.
How many maintainers does it have?
Is there only one person working on this code? What happens if they get really busy?
Are there lots of open issues or pull requests?
If the code’s on GitHub, take a look at the issues and pull requests. Does it seem like these are neglected, or is the project team responsive and present?
There’s no hard and fast rule for assessing the health of a project, but thinking along these lines should allow you to get a feel for whether code’s going to be well maintained.
Rule 3: do you still need it?
We might find that our code evolves. We might find that platforms evolve. In those cases we need to keep on top of our dependencies to make sure that we’re not bringing anything in unnecessarily. Particularly in cases where native implementations become available, we should use that to get a speed boost for free.
Rule 4 of software dependency management: keep up to date!
Rules 1 & 2 are mere preludes to the killer pain of dependency management: updates. Let’s look at an example using Ruby on Rails 6, which introduced Action Cable to integrate WebSockets into your application. You try and upgrade Rails as follows:
bundle update rails
And see the dreaded:
Bundler could not find compatible versions for gem "actionpack" <INSERT ENDLESS MESSAGES>
This is the reality of dependency hell. A gem relies on actionpack 5.x, so when you try and upgrade actionpack to 6.x you can’t, because of that one gem. So, you update that gem, but you can’t, because either it hasn’t been updated for Rails 6 or it in turn depends on another gem which is tied to Rails 5. Repeat this all the way down the dependency tree or until you have removed all of your hair and nails, whatever comes first.
There is no foolproof way around this, but the easiest way is to follow the other rules: keep your dependencies limited to ones that you really need and ones which are well maintained. Secondly, don’t pin to some random broken git revision, and don’t use your own forks of code. You’re asking for trouble. Thirdly, make updating your dependencies routine. Do it regularly and you’ll avoid having to wade through dependency hell and migration instructions in changelogs. Not only will your hair and nails thank you, but your project will too.