Dependency hell: a complete guide
At a recent meetup, a few of us developers got into a discussion about "dependency hell"—the condition when one or more pieces of software need two or more conflicting dependencies (usually transitive ones) installed in the same environment.
Almost every developer has run into some version of this: library A depends on version 1 of a piece of software, but library B depends on version 2. The latest video card driver works for one game, but fails on a different game, requiring a downgrade (until the vendor fixes the issue), the list goes on.
I started recanting a story about a time I ran into a dependency hell situation, and I randomly blurted out, "Everyone's going to get into dependency hell; don't be ashamed, it happens to everyone!"
We’re all going to (dependency) hell
You pinned a dependency for a reason and never got around to updating it. You forked a project to add some functionality and the upstream package moved merrily along without your changes. A native extension needs one version of a system library, and a second app on the system needs another. The road to dependency hell is paved with good intentions, those intentions being "get this thing working so we can achieve our organizational goals."
It's going to happen to you, it's going to rear its ugly head at the worst time, and it's going to be painful to escape.
Dependency hell is not technology specific either. I've run into it in the Ruby/Rails ecosystem, in the Clojure ecosystem, and in the NodeJS ecosystem. I know folks who have run into it in C++ and Python, too. You name the language, operating system, framework...it's going to happen.
The way out
I wish there was one simple answer, but the paths out of dependency hell lead all over the place:
-
Manually experimenting with (or git bisecting) the versions of dependencies in your manifest file until things get happy (how are those integration tests working for you? Time to strengthen them up a bit, maybe?)
-
Forking and fixing open source dependencies on your own repos, hopefully being able to push those changes back upstream into the main releases.
-
If, for some reason, the maintainer can't/won't accept the patch, you've got two choices—stick with this dependency, forever git rebaseing to keep it up to date with your changes; or saying YOLO and never touching this forked dependency again.
-
Finding or writing a replacement dependency, one that requires as little code change as possible (I ask again, how's that integration test suite?)
The technical aspects of fixing dependency hell are frustrating, difficult, and not fun. There's another aspect that you'll probably experience, only because your brain is trying its best to protect you from being kicked off the proverbial island.
Shame vs. guilt
Shame is a feeling associated with a negative evaluation of yourself. While there are several “flavors” of shame, the one we’re talking about here is the one that arises when you break a social norm. You want to keep in good graces with the community around you (your coworkers, boss, and company), so you’re compelled to believe yourself faulty in order to change your behavior and fix what went wrong. It's different from guilt, a feeling you get when you break a personal code of ethics. Shame is all about what you believe others think of you. For example:
-
You feel guilty when you declared that you'll keep the integration test suite running to catch dependency hell issues and you find that you've let it lapse (see, I kept asking you about it for a reason). Your body primes you to hide your mistake so you don't get caught, or you must muster the strength to fess up.
-
You feel shame when, upon finding this out, your team confronts you about the failures of the integration suite. You don't want to have to face looking like a fool in front of your coworkers, so your body primes you to either deflect and point the finger at others, or to own up to the fact you disappointed your colleagues and that you'll work to make it right.
While a little bit of shame in these situations is normal and unavoidable, it's when we are left swimming in it for long periods that it becomes a bad thing. Dwelling on what others may think of you and latching on to “what if” scenarios prevents you from working well with others, and with yourself, to get the issue resolved.
Finally, you're probably going to have some physical effects with shame—blushing, turning away from others, a quiet voice. It's just your brain and body doing what it decided was an appropriate response to this situation tens of thousands of years ago. It's perfectly natural.
Dependency hell is guaranteed to happen
We aim big at Tidelift. We want to make your dependencies as safe to combine in whatever way you can possibly want to combine them. We want to work with maintainers to make their projects as safe as possible for subscribers to combine together to make their amazing apps.
We at Tidelift, however, are not perfect (for shame!). Nor are the maintainers who become lifters. Nor are the maintainers who work on non-lifted dependencies. Nor is the universe.
There will eventually be a combination of factors that cause the myriad of dependencies in your application to get into gridlock. You and your team will be stuck in dependency hell, quite possibly through no fault of your own. And it's gonna suck, and it's gonna take time to unravel. At that point, acceptance of the situation and overcoming your shame are the best approach to get a project, a team, and yourself, through the gridlock in one piece.
Acceptance for managers
If you are a manager, the worst thing you can do when dependency hell rears its head is start pointing fingers. While a post-mortem to determine what happened and how to hopefully prevent it in the future is both useful and essential, and while a little bit of pressure to get a problem fixed isn't a bad thing (a 100% stress-free job can really prevent growth), nothing demotivates an already demotivated team better than publicly shaming them (or an individual) for a problem that they might have not even been able to predict.
A compassionate-yet-stern approach where the whole team (including yourself) is responsible for fixing it will accomplish much more, and can help turn an awful experience into something that, in a weird way, could be looked back upon fondly months or years from now.
Acceptance for teams
The right approach to facing a dependency hell challenge is to take an attitude akin to “welcome to the firefight,” where your combined goal is to fix this problem as efficiently and comprehensively as you can so you can get back to the business of delivering features. Everyone will take their piece of the puzzle, whether it be improving test suites, trying different version combinations, or rewriting code to work around dependency issues, and you all will bring it together into a stable fix.
Much like the advice for managers, the last thing you should do is angrily point the finger at a teammate to shame and blame. Yes, someone might have introduced an issue for the reasons listed above, but unless they were cackling like a mustached villain who tied your project's Git repo to some train tracks, the choice they made was probably the best they could make with the information they had at the time. Software development is always about tradeoffs, and you can never know the full effect of a choice made now months or a few years down the road. Be kind and understanding and you’ll get this situation fixed up a lot sooner.
Acceptance for you!
Finally, take care of yourself. An encounter with dependency hell is not the time to drive yourself into the ground. It is not the time for slamming tons of caffeine to keep yourself going beyond your limits. Eat well, take a break to exercise (even a walk around the building or block is useful), and rest when you can, because if you're tired, you'll make mistakes and start losing your temper, which will not get the problem solved any sooner.
I’ve experienced dependency hell many times in my career, and have learned, sometimes the hard way, that the best way forward is the one where accepting the situation for what it is—as inevitable—and working together as a team to resolve it and prevent it as best as possible in the future, is the one where everyone makes it through happy and healthy.
Learn how Tidelift can help your team navigate open source dependencies and how we partner with open source maintainers.