The Chaos of Maintaining Software Dependencies and How to Tame Them

Mgs M Rizqi Fadhlurrahman
Inside Bukalapak
Published in
12 min readApr 18, 2023
software-dependencies-mindblowing

I’ve been through a lot of chaotic relationships with software dependencies, and maybe you too. Over the years, I’ve accumulated various strategies to handle that and make them manageable. I’d like to share them with you here so that you can tame the chaos of maintaining software dependencies on your projects.

Let’s start with Dependency Hell:

dependency-hell

Dependency hell is a colloquial term for the frustration of some software users who have installed software packages that have dependencies on specific versions of other software packages.

Now, let’s look at the dependencies of the popular Nuxt framework below.

nuxt-dependencies-1

And look if we zoom out.

nuxt-dependencies-2

And zoom out again.

nuxt-dependencies-3

Wow, that escalated quickly. I know you can’t see anything there.

And if we spread out the graph, it becomes a constellation of software dependencies. 😂

nuxt-dependencies-graph
nuxt-dependencies-detail

Before we go any further, let’s look at the definition of software dependency:

1. What is software dependency?

TLDR: Another piece of software that we reuse as the foundation of our project.

A software dependency is an external standalone library that can be as small as a single file or as big as multiple files and folders organized into packages to perform a specific task. [1] Software dependencies may be shared between different software applications and enable features not present in the application itself. Software dependencies can be installed through one of the many available dependency managers or may be included in the same application package as the primary software component.

A dependency manager, or package manager, is a software module that helps integrate external libraries or packages into your larger application stack. [1] Dependency managers make working with third-party software libraries easier, as they remove the requirement to track down every individual library file and copy it over to your application codebase. All dependency managers work in roughly the same way: they fetch the desired packages from an online repository (such as npm, PyPI, or Maven Central), which are then extracted and configured to be used by our software.

Here are some examples of popular package managers for each programming language:

  • gradle > Java
  • bundler/gem > Ruby
  • npm/yarn > Javascript
  • composer > PHP
  • pip/pipenv > Python
  • nuget > C#
  • gomod > Go

2. Why do we need to maintain software dependencies?

TLDR: We don’t want to let our software degenerate or be hard to upgrade, then eventually die and need a rewrite.

Software rot, also known as bit rot, code rot, software erosion, software decay, or software entropy, is either a slow deterioration of software quality over time or its diminishing responsiveness that will eventually lead to software becoming faulty, unusable, or in need of an upgrade. [2]

There are several reasons why we need to maintain our software dependencies:

  • Security Issues
    Some libraries have known vulnerabilities. If you’re not updating these libraries, your application is exposed and you may be passing this risk on to others.
  • Performance Improvements
    If your dependencies are outdated, you may be missing out on the latest enhancements that significantly improve performance or add new functionality.
  • Quality Assurance
    To keep your application running smoothly, you need to prevent dependency problems from being introduced in the future, as well as keep your software up-to-date on dependencies that have already had bugs fixed or are in the phase of end-of-life.

Someone out there may still be thinking:

“My software is working fine, why do I need to update the dependencies? I have other priorities to do.”

Or maybe:

“I fear the build is broken if I update the dependencies. Updating the dependencies is hard.”

Here, I will give you more technical reasons:

The Chaos of Dependency Hell

  • Many dependencies
    If a software depends on many libraries, it will require lengthy downloads and large amounts of disk space.
  • Long chains of dependencies
    If a software depends on A, which depends on B, …, which depends on Z.
  • Conflicting dependencies
    If a software depends on A and B, A depends on C v1, and B depends on C v2, and different versions of C cannot be simultaneously installed, then A and B cannot simultaneously be used.
conflicting-dependencies
  • Circular dependencies
    If a software depends on A, and A depends upon and can’t run without a specific version of B, but B depends upon and can’t run without a specific version of A, then upgrading any dependency will break another.
circular-dependencies
  • Diamond dependencies
    When library A depends on libraries B and C, both B and C depend on library D, but B requires version D v1 and C requires version D v2. The build fails if there is only one version of D installed.
diamond-dependencies

There is also an article from SourceGraph Blog that explains dependency hell in an interesting way. They say that there are nine levels of dependency hell. Each circle denotes a more evil transgression of package management: Limbo, Lust, Gluttony, Greed, Wrath, Heresy, Violence, Fraud, and Treachery. You can read the details here:

The Nine Circles of Dependency Hell (and a roadmap out).

If you still think that this chaos is still avoidable when you don’t touch the codebase, here are some of the real cases of why you need to maintain your codebase as well as your dependencies:

If those security issues arise, and you depend on one of those libraries in your software, you have to update your dependency to the latest version to prevent any business impact on your side. The chaos is happening when your major version gap is too far from the latest version that includes the security patch. Updating a major version is not an easy task. You can’t escape from a breaking change introduced by updating a major version. The updated dependency may also be conflicted with another dependency and may introduce a diamond dependency problem. It will be a hard time for you to make your dependencies work together and fix those breaking changes in your codebase.

“It’s much easier to apply a patch when it’s really critical if you’re closer to the latest version” — Maya Kaczorowski (was a Senior Director of Product Management and Software Supply Chain Security at GitHub)

3. How to maintain software dependencies?

Dependency management is a technique for identifying, resolving, and patching dependencies in our application’s codebase. By identifying what we depend on, we’re better prepared to identify our application’s inherent vulnerabilities and understand how they might affect us.

Here are the several steps to follow when you’re updating your dependencies:

  1. Check if there are any updates from your dependencies
  2. Run and test the project on your local computer
  3. Fix deprecation warning if any
  4. Read the changelogs, look for breaking changes
  5. Fix breaking changes if any, then run and test again on your local computer
  6. Run and test the project in preproduction

How to check if there are any updates from your dependencies? Here are some examples of dependency checking in various programming languages that I frequently use:

# npm/yarn
npm install -g npm-check-updates
ncu -t minor

# ruby bundler
bundle outdated --minor --only-explicit
bundle update --conservative

# golang
go list -u -m -f '{{if not .Indirect}}{{.}}{{end}}' all
ncu-example

You can update your dependencies’ minor versions at once if your dependencies are well maintained. The minor version usually doesn’t have breaking changes, so it’s usually safe. But, it still has some risks if one of your dependencies does not follow the semantic versioning rules. Be careful while updating the dependency minor version if it’s still in version 0.x because it may have a breaking change.

Tips for Maintaining Software Dependencies

  • Do periodic dependency maintenance
    Upgrade the dependency whenever possible, preferably to the most recent major version. If you regularly update and maintain your dependencies, it will be convenient for you to do so in the future. And if a dependency-related emergency arises, you can deal with it quickly because there won’t be a significant breaking change brought on by the fact that the major version of your dependency is out of sync with the la version.
  • One giant leap vs. a few small hops
    It is preferable to bump individual dependencies incrementally rather than all of them simultaneously. The best chance to avoid breaking your software is to upgrade your dependency’s major versions one at a time. An incremental update for each dependency is better than bumping all of your dependencies at once. Upgrading dependency major versions one by one is the best chance to not break your software.
  • It’s better to have more than 80% test coverage
    For some people, having test coverage of more than 80% is just a number. However, if you’re updating your dependencies, you’ll need adequate coverage to ensure that the update doesn’t harm your software.
  • Use staging or preproduction environment
    “Real men test in production”? Please, no.
  • Don’t regenerate the lock file
    If you need to reinstall a dependency and want to keep using the same version, only remove it from the node modules/vendors folder and leave your lock file intact. In essence, deleting your lock file is equivalent to nuking all of your dependencies as well as your software because it will reinstall your dependencies with the latest version, and it may have a breaking change
  • Risk of early adopters
    Avoid updating dependencies too soon after they are released. The risk of early adopters exists. If you really need that update, hold off until a few patches are released, or just wait a few days.
  • Avoid unexpected breaking changes
    Always face the breaking changes with a plan. If necessary, lock the major version of your dependency to prevent a coworker or even you from unintentionally upgrading it and breaking your software. For example, to lock the major version of Ruby Gems, use `~>` instead of `>=` in the Gemfile.
  • Maintain the dependencies automatically
    Use automatic dependency maintenance bots like Dependabot, Renovate, Greenkeeper, or Synk.

Dependabot

Dependabot is currently available for free on Github. We only need to add a configuration file to get Dependabot up and running. In this configuration file, we need to mention the package manager and the directory that Dependabot will monitor. The Dependabot will generate pull requests for updated dependencies while it is running. If the pull requests are only for minor or patch version updates and our test pipeline passes, we can quickly review and merge them from time to time. We must conduct some sanity checks on our preproduction environment if the pull request contains a major version update, even though our pipeline has passed to ensure that the update will not break our software. As a result, our dependencies will be well maintained.

dependabot-example

Dependabot Runner

In Bukalapak, we are currently using Gitlab Enterprise to store our git repositories and run devops pipelines. There is an open-source package that can be used to run Dependabot on Gitlab because it is not by default available here:

dependabot. Dependabot-Gitlab has two types of usage: service mode and standalone mode. In service mode, we can deploy the Dependabot docker container on our server. In standalone mode, we can utilize Dependabot rake tasks to run in our scheduled GitLab pipelines. However, the standalone version is limited to the basic dependency update feature.

By extending the Dependabot-Gitlab standalone pipeline, we created Dependabot-Runner to integrate it with our current pipeline. The Dependabot-Runner provides some features, such as:

  • Pipeline job to run dependabot
  • Automatic repo registration
  • Automatic closure of outdated MRs
  • Repository statistics dashboard
  • Script to update dependabot upstream
dependabot-runner-repo
dependabot-runner-pipeline
dependabot-runner-merge-requests
dependabot-runner-dashboard
dependabot-runner-diagram

Dependabot-Runner is still in the Proof of Concept stage right now. A number of services are already using it. However, because Dependabot-Gitlab is still in version 0.x, there are still some bugs in it. As a result, we may wait to roll out Dependabot-Runner to the other services until it is safe and reliable enough for us to do so.

Conclusion

We are already aware of how dependency hell can harm our software and how software rot can be a ticking time bomb in terms of potential future risk. We also learned how to maintain our dependencies to avoid dependency hell and software rot issues. We can use an automation tool like Dependabot to assist us in resolving dependency-related issues before they cause harm to our software.

Hopefully, this article will inspire you to keep yourself motivated to maintain your software dependencies and prevent any issues in the future. I would appreciate your feedback in the comments! Thanks for reading!

dependency-hell-vs-automatically

References

[1] A. Sharma, “Dev corrupts NPM libs ‘colors’ and ‘faker’ breaking thousands of apps,” BleepingComputer, Jan. 09, 2022. https://www.bleepingcomputer.com/news/security/dev-corrupts-npm-libs-colors-and-faker-breaking-thousands-of-apps/ (accessed Aug. 22, 2022).

[2] B. Sourour, “Code dependencies are the devil.,” freeCodeCamp.org, Oct. 26, 2016. https://www.freecodecamp.org/news/code-dependencies-are-the-devil-35ed28b556d/ (accessed Aug. 22, 2022).

[3] E. Georgescu, “Software Rot: Definition, Causes, Threats, Mitigation Methods,” Heimdal Security Blog, Oct. 23, 2020. https://heimdalsecurity.com/blog/software-rot/ (accessed Aug. 22, 2022).

[4] G. Polasani and S. R. Rubin, “Embedded Malware in NPM: Coa, Rc, Ua-parser — FOSSA,” Dependency Heaven, Nov. 08, 2021. https://fossa.com/blog/embedded-malware-npm-coa-rc-ua-parser/ (accessed Aug. 22, 2022).

[5] K. Collins, “How one programmer broke the internet by deleting a tiny piece of code — Quartz,” Quartz, Mar. 27, 2016. https://qz.com/646467/how-one-programmer-broke-the-internet-by-deleting-a-tiny-piece-of-code/ (accessed Aug. 22, 2022).

[6] M. Rickard, “The Nine Circles of Dependency Hell (and a roadmap out),” The Nine Circles of Dependency Hell (and a roadmap out), Sep. 29, 2021. https://about.sourcegraph.com/blog/nine-circles-of-dependency-hell (accessed Aug. 22, 2022).

[7] R. Arkins, “Dependency Management: 3 Tips to Keep You Sane | Mend,” Mend, Mar. 19, 2020. https://www.mend.io/free-developer-tools/blog/dependency-management/ (accessed Aug. 22, 2022).

[8] S. Killeen, “Automagically Update Your Package Dependencies With Dependabot — For Free! — SeanKilleen.com,” SeanKilleen.com, Jul. 04, 2019. https://seankilleen.com/2019/07/automagically-update-your-package-dependencies-with-dependabot-for-free/ (accessed Aug. 22, 2022).

[9] S. Lemay, “How to Manage Dependencies with Confidence — Skyler Lemay,” How to Manage Dependencies with Confidence — Skyler Lemay, Feb. 29, 2019. https://www.skylerlemay.com/blog/2019/02/25/how-to-manage-dependencies-with-confidence (accessed Aug. 22, 2022).

[10] S. Rubin and M. Schwartz, “Log4J ‘Log4Shell’ Zero-Day Vulnerability: Impact and Fixes — FOSSA,” Dependency Heaven, Dec. 10, 2021. https://fossa.com/blog/log4j-log4shell-zero-day-vulnerability-impact-fixes/ (accessed Aug. 22, 2022).

[11] Tammy Xu, “How to Manage Software Dependencies | Built In,” Built In, Jan. 25, 2022. https://builtin.com/software-engineering-perspectives/dependabot (accessed Aug. 22, 2022).

[12] Z. Bloom, “JavaScript Libraries Are Almost Never Updated Once Installed,” The Cloudflare Blog, Jan. 27, 2020. https://blog.cloudflare.com/javascript-libraries-are-almost-never-updated/ (accessed Aug. 22, 2022).

--

--