Dependency Management
Most of what you ship was written by strangers, resolved by an algorithm you did not choose, and updated on a schedule you have to decide.
The question, the obvious approach, and why it breaks
Every lesson starts where the work starts: an operational problem, a first attempt that is entirely reasonable, and the way production disagrees with it.
What is actually in this build, who decided which versions, and how does that set change over time?
A project declares a handful of direct dependencies and ships hundreds of transitive ones. The declared list is written by you; the shipped list is produced by a resolver, and the two are frequently confused.
We list our dependencies in the manifest with caret ranges so we get bug fixes automatically. The package manager handles the rest.
The manifest lists direct dependencies. The build ships the transitive closure, which is usually an order of magnitude larger and which nobody has read (Dependency Security).
- The manifest lists direct dependencies. The build ships the transitive closure, which is usually an order of magnitude larger and which nobody has read (Dependency Security).
- A caret range delegates a production decision to whoever publishes next. A patch release with a behaviour change lands in your build with no commit on your side (Dependency Pinning).
- "Automatically get bug fixes" is symmetrical: you automatically get regressions, and you get them at build time rather than at review time.
- Resolution is ecosystem-specific and surprising. Whether two versions of one library can coexist, and which version wins a conflict, differs completely between npm, Maven, Go and Cargo.
- Without a lockfile the resolver runs again on every machine, so a developer, CI and the release build can each produce a different closure from the same manifest (Reproducible Builds).
- Never updating is the other failure: a dependency four years behind cannot be patched for a vulnerability without an upgrade that is now a project.
What is actually happening
Underneath the tooling, which is the part that survives a change of tool.
- There are two graphs. The declared graph is what your manifest says. The resolved graph is what the resolver produced. The lockfile is a written record of the second, and without one it is recomputed — possibly differently — every time.
- Resolvers differ on the two questions that matter: can multiple versions of the same package coexist, and how is a conflict decided? Those two answers explain most cross-ecosystem confusion.
- Version ranges are a communication protocol between publishers and consumers, and it is only as reliable as publishers' discipline about what a patch release means (Semantic Versioning, and Where It Stops Applying).
- Update cadence is a real engineering choice with a U-shaped cost curve. Continuous small updates are cheap individually and constant; batched updates are rare and large; never updating converts to an emergency the day a vulnerability is announced.
- Direct and transitive dependencies need different treatment. You can choose your direct ones; your leverage over transitive ones is limited to overrides, or to replacing the direct dependency that pulled them in.
- An internal mirror or proxy changes the trust and availability model: builds no longer depend on a public registry being up, and you gain a place to enforce policy (Securing the Pipeline Itself).
The graph you did not write
The shape below is why "we have twelve dependencies" is almost always wrong, and why a diamond — one package reached by two paths at different versions — is the normal case rather than an edge case.
What your ecosystem does at node util is the single most important thing to know about it.
Resolvers do not agree
These are the mechanics behind the diamond above. Whether two versions coexist and how conflicts are decided are the two questions, and every ecosystem answers them differently.
The practical consequence: an upgrade that is a routine bump in one ecosystem is a coordinated migration in another, and advice transfers badly between them.
| Ecosystem | Multiple versions of one package? | Conflict resolved by | Lock record |
|---|---|---|---|
| npm / yarn / pnpm | Yes — nested installs let versions coexist | Duplication rather than a decision; hoisting affects which one is found | package-lock.json / yarn.lock / pnpm-lock.yaml, with integrity hashes |
| Go modules | One version per module path per build | Minimal version selection — the highest version any dependency requires | go.mod plus go.sum hashes |
| Maven | One version per artefact | Nearest definition in the dependency tree wins, not the highest | pom.xml plus optional dependency management sections |
| Cargo | Semver-incompatible majors coexist; compatible ranges unify | Unification within a compatibility range | Cargo.lock |
| pip / Python | One version per distribution name in an environment | A backtracking resolver, which can fail outright rather than pick | Not native — requirements pinning, pip-tools, Poetry or uv supply it |
Choosing an update cadence
This is the decision most teams make by default rather than deliberately, and the default is "never, then all at once during a security incident".
There is no correct answer, but there is a wrong one: no cadence at all, which resolves itself into the last option below at the worst possible time.
A dependency publishes a new version. When does it reach your build?
when A bot opens one pull request per update; the team reviews and merges as routine work. The default for most teams.
cost Constant low-level review load. Degrades into an unread backlog unless someone owns it (Toil).
when Your test suite is trusted and your flake rate is low enough that a green build means something.
cost You have delegated a production decision to CI. A publisher's mistaken patch release reaches you unread (CI Is a Feedback System).
when Small teams, or ecosystems where updates are usually uneventful and review attention is the scarce resource.
cost Each batch bundles many changes, so a regression has many suspects. Vulnerability response is slower by up to the batch interval (Change Correlation).
when A frozen system near end of life with a deliberate decision behind the freeze.
cost Each forced upgrade is large and unrehearsed, and it happens under time pressure because an advisory forced it (Vulnerability Management by Exposure).
How to do it properly
Most important first.
- Commit the lockfile and treat changes to it as reviewable. A pull request that moves two hundred transitive versions deserves the same scrutiny as one that changes two hundred lines.
- Install from the lockfile in CI, not from the manifest — most package managers have a distinct command for this, and using the wrong one silently re-resolves.
- Count your direct dependencies and question each one. A dependency for a function you could write in ten lines brings its entire subtree with it.
- Automate update proposals as small, individually reviewable pull requests so each change has an identifiable blast radius (Change Size: Why Small Changes Are Safer, and When They Are Not).
- Separate the automation from the merge policy: auto-merging patch bumps is reasonable only if your test suite would actually catch a regression (Flaky Tests).
- Mirror or proxy the registries you depend on, so a public outage does not stop your builds and a removed package does not break your history (Build Environments).
- Disable lifecycle scripts during install where the ecosystem allows it; arbitrary code executing at install time is the shortest path from a compromised package to your build (Typosquatting and Malicious Packages).
How much can this affect
Every production change has a blast radius. Stated as a scale so it is comparable between changes rather than adjectival — and paired with what actually contains it, because a wide scope with a real containment mechanism is a different situation from a wide scope with none.
A dependency change affects every artefact built after it. Containment is the lockfile (the change is explicit and reviewable) and progressive rollout of the resulting build (Canary: One Percent, Then Five, Then Watch).
What can go wrong
- Lockfile not committed, or committed and then bypassed by an install command that re-resolves.
- Update bot pull requests accumulating unread, so the automation produces noise instead of currency (Alert Fatigue).
- A transitive dependency deprecated or removed from a public registry, breaking builds of commits that used to work.
- Diamond dependency conflicts where two direct dependencies need incompatible versions of a shared library, and the resolver silently picks one.
- An internal mirror configured as a fallback rather than as the source, so builds still reach the public registry when the mirror misses.
- Vendored dependencies edited in place, so the vendor directory no longer matches the lockfile and nothing detects it.
- A single maintainer's account compromise reaching production because install-time scripts run with the build's privileges (CI Security).
- "We only have twelve dependencies." You have twelve direct ones. Print the resolved closure before believing the number.
- "Semantic versioning means patch releases are safe." It means the publisher intended them to be. It is a convention with no enforcement (Semantic Versioning, and Where It Stops Applying).
- "The lockfile is a build artefact." It is a decision record about what you ship, and it belongs under review.
- "Fewer dependencies is always better." Reimplementing cryptography or date handling to avoid a dependency is a far larger risk than the dependency was.
Operating it
Evidence is the signal, not the intention. Rollback is sometimes 'you cannot, and that is the point'.
- A clean install on a fresh machine produces the same resolved tree as CI — compare the lockfile-derived closure, not the manifest.
- You can list every package in the artefact with its version, from a stored record rather than by rebuilding (Software Bill of Materials).
- The age distribution of dependencies is visible, so "how far behind are we" is a number.
- A build with the public registry unreachable succeeds, because the mirror is the source rather than a fallback.
- Update pull requests are merged or closed within a defined window; the backlog is bounded.
- A dependency change reverts by reverting the lockfile and the manifest together. Reverting only one leaves them inconsistent, which is worse than either state.
- Keep a mirror or a local cache so reverting to an older version still works even if it was yanked from the public registry — otherwise your rollback path depends on someone else's retention policy.
- For an upgrade that has already shipped, rolling back the dependency may require rolling back the code that used its new API; if those landed in one change, the revert is all-or-nothing (Change Size: Why Small Changes Are Safer, and When They Are Not).
- Automate proposal, testing and reporting of updates. This is the clearest case for automation in the module: mechanical, frequent, and well covered by tests.
- Automate advisory matching against the resolved closure so a published vulnerability produces a list of affected artefacts rather than a research task (Scanning, and Why a Finding Is Not a Risk).
- Automate license and policy checks at install time if you have such policies, since after the fact is far more expensive (Policy as Code).
- Keep the decision to adopt a major version human. A major bump is an API migration wearing a version number (Backward Compatibility: The Real Rules).
- Frequent small updates keep you current and consume continuous review attention; batched updates preserve attention and make each batch riskier.
- Fewer dependencies means less attack surface and more code you maintain yourself, with its own defects.
- An internal mirror buys availability and policy control at the cost of running a registry.
- Auto-merging patch updates removes toil and delegates a production decision to your test suite — which is only as good as your flake rate (Flaky Tests).
Where this applies
This domain is unusually tool- and organisation-dependent. These labels say what each claim is specific to, and what a different platform, provider or organisation does instead.
- TOOL-SPECIFICResolution semantics differ fundamentally by ecosystem — see the table below. Advice written for npm frequently does not apply to Go or Maven, and the failure is silent because the vocabulary is shared.
- ORG-SPECIFICUpdate cadence and auto-merge policy are organisational risk decisions. A team with a strong, non-flaky test suite can auto-merge patch bumps safely; the same policy on a suite with a 3% flake rate means changes merging on a coin flip.
Where the depth lives
This domain teaches delivery and operation, and hands the mechanism off to the domain that owns it.