Managing dependencies
Adding a dependency on existing code seems like a simple decision. Don’t repeat yourself (DRY) is a commonly taught principle. Why should we all write our own left-pad? Database drivers, application frameworks, machine learning packages—there are many examples of libraries you should not write from scratch. But dependencies bring risk: incompatible changes, circular dependencies, version conflicts, and lack of control. You must consider these risks and how to mitigate them.
Dependency Management Basics
A dependency is code that your code relies on. The time at which a dependency is needed—during compilation, testing, or runtime—is called its scope.
A good versioning scheme has versions that are:
UNIQUE Versions should never be reused. Artifacts get distributed, cached, and pulled by automated workflows. Never republish changed code under an existing version.
COMPARABLE Versions should help humans and tools reason about version precedence. Precedence is used to resolve conflicts when a build depends on multiple versions of the same artifact.
INFORMATIVE Versions differentiate between prereleased and released code, associate build numbers to artifacts, and set stability and compatibility expectations.
Semantic Versioning
(See for details: https://semver.org/)
Given a version number MAJOR.MINOR.PATCH, increment the:
MAJOR version when you make incompatible API changes,
MINOR version when you add functionality in a backwards compatible manner, and
PATCH version when you make backwards compatible bug fixes.
Note
Prerelease
Major version 0, considered “prerelease,” is intended for fast iteration.
SemVer also defines prerelease versions by appending a - character after the patch version. Dotseparated alphanumeric sequences are used for prerelease identifiers (2.13.7-alpha.2).
Release candidates
Many projects use release candidate (RC) builds. Early adopters can find bugs in RCs before the official release. RC prerelease versions have incremental identifiers, such as 3.0.0-rc.1. The final RC is then promoted to the release version by re-releasing it without an RC suffix. All prereleased versions are superseded by the final release (3.0.0 in our example).
Build number
Build numbers are appended after both the version and prerelease metadata: 2.13.7alpha.2+1942.
Transitive Dependencies
Dependencies usually depend on other libraries, which become transitive dependencies. A dependency report shows the fully resolved dependency tree (or dependency graph).
Avoiding Dependency Hell
Ask yourself if a dependency’s value outweighs its cost.
Do you really need the functionality?
How well maintained is the dependency?
How easy would it be for you to fix the dependency if something went wrong?
How mature is the dependency?
How frequently does the dependency introduce backwardincompatible changes?
How well do you, your team, and your organization understand the dependency?
How easy is it to write the code yourself?
How is the code licensed?
What is the ratio of code you use versus code you don’t use in the dependency?
Use these tips to avoid dependency hell.
Isolate Dependencies: Copy paste code, shade the dependency in another namespace
Deliberately Add Dependencies: Explicitly declare as dependencies all libraries you use
Pin Versions:
Scope Dependencies Narrowly: Prod, dev, testing etc.
Protect Yourself from Circular Dependencies.
Note
Sources:
The Missing README: A Guide for the New Software Engineer © 2021 by Chris Riccomini and Dmitriy Ryaboy, Chapter 5.