Dep MgmtGENERALLANGUAGE-SPECIFICCONTESTED

Dependency Management

Every package in the lockfile is code you now operate without having written it — and most of it arrived without anyone making a decision.

The requirement, the obvious build, and why it breaks

Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.

The question

What am I actually taking on when I add a dependency, and how do I keep that decision reversible?

The requirement

Date formatting with timezone support is needed by Friday. Someone opens a pull request that adds a library, CI goes green, and it merges four minutes later.

The obvious build

A dependency is just a library we call. Adding one is like adding an import — it is free until it breaks, and if it breaks we will deal with it then. Not reinventing wheels is good engineering.

Why it breaks

It is free at install time and expensive at *upgrade* time, and upgrade time is not chosen by you. A CVE, a runtime deprecation or a peer-dependency conflict sets the date.

How it breaks as requirements change
  • It is free at install time and expensive at *upgrade* time, and upgrade time is not chosen by you. A CVE, a runtime deprecation or a peer-dependency conflict sets the date.
  • The cost is not proportional to how much you use. A package called from forty files is forty edits to remove even if each call is one line, and the number of call sites is the only variable you control.
  • "We will deal with it then" assumes the person who added it is still here and still remembers which of the library's behaviours you were relying on. Usually neither holds (Bus Factor).
  • The obligation is continuous, not one-off: you have signed up to track this project's releases forever, and the ones you skip compound into an upgrade that is a project rather than a bump.
  • Not reinventing wheels is good engineering. It stops being an argument the moment the wheel is thirty lines and the package is thirty thousand (Do We Need a Package for This?).
RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

What limits the solution, and what must never stop being true

This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.

Constraints
  • Friday is real and comes out of committed delivery, so "write it ourselves" has to compete on the same clock.
  • The package manager installs transitively by default. You acquire the whole graph whether or not anyone looked at it.
  • Security policy requires known-vulnerable packages to be patched within a fixed window of disclosure — an obligation that attaches to the entire tree, not only to what you chose.
  • Nobody has budget to audit four hundred packages, so any process that assumes an audit will not be followed.
Invariants
  • The build is reproducible: the same commit resolves to the same bytes on every machine and in CI, or nothing else here can be verified (Stability and Dependency Direction).
  • Every direct dependency has a person on this team who can say why it is there. A dependency nobody can justify is a dependency nobody will dare remove.
  • Nothing enters the running system without appearing in the lockfile that review can see.

Who owns what, and where the seams fall

Responsibilities decide boundaries; boundaries decide what an interface has to say.

Responsibilities
  • The team owns every direct dependency: knowing why it is there, tracking its releases, and being able to answer "what happens if this is abandoned".
  • The direct dependency's maintainer owns its transitive tree in the sense of choosing it — and you own it in the sense of running it (Transitive Dependencies).
  • One module in your codebase, not forty, should own the knowledge of *how* a volatile dependency is called, so that replacing it is an edit rather than a search (Volatile Dependencies).
  • Nobody owns a dependency that was added by someone who has left and is imported in two files. That is the state to avoid, and it is the default state.
Boundaries
  • The seam that matters is between your domain code and the dependency's vocabulary. If the library's types appear in your function signatures, the boundary is everywhere (Leaky Abstractions).
  • Not every dependency deserves a seam. A wrapper around a JSON parser buys nothing; a wrapper around a payment SDK or an ORM buys a replaceable component (Boundary Adapters).
  • The discriminator is volatility and reach, not size. Wrap what is likely to change under you and is called from many places; call the rest directly and be honest that you are coupled to it.

The one-line diff and the four-hundred-line lockfile

Adding a dependency is the cheapest action in software and one of the most durable. The pull request is one line in a manifest; the consequence is a permanent obligation to track a project you do not control, and a bill that arrives on the maintainer's schedule rather than yours.

The useful question is not "is this library good". It is: when this library ships a breaking change, what does that cost us? That number is decided by how many of our files know it exists — which is a design decision, made now, in this pull request.

The HTTP client we use ships a major version that changes its error type
The change

The library's 3.0 replaces thrown errors with a result object and renames two options. There is a CVE in 2.x, so staying is not an option.

The client is imported directly wherever a call is made
CheckoutServiceWebhookSenderImportJobAdminSyncSearchProxyNotificationWorkerHealthCheckReportExport
testseight test files, each stubbing the library's own error shape
8 modules · 1 test file

Eight edits, and the expensive part is that each site handled errors slightly differently — so the migration is eight small design decisions taken under a disclosure deadline by whoever is free.

One `http/` module owns the client; the rest of the code sees our own request and failure types
http/Client
testshttp_client_testone integration test against a local server
1 module · 2 test files

One file changes. Every caller keeps compiling because none of them ever named the vendor's error type. The upgrade becomes a normal change instead of an incident.

what it cost The owning module is a real abstraction with one implementation, and it is worse than the library in two specific ways: it exposes a subset, so the first caller who needs streaming or a custom retry hook has to widen it or go around it; and it adds a hop for every reader tracing a request. If the library never breaks, that is cost with no return — this move is a bet that it will.

What actually goes wrong, and what to do about each

Dependency pain is not one failure. It is five, with different triggers, different owners and different responses, and a team that treats them as one thing responds to all of them by upgrading everything and hoping.

Notice how many of these are triggered by something outside your repository. That is the defining property: you do not choose when to spend this money.

Five ways a dependency costs you money
TriggerSymptomCauseResponse
A CVE is disclosed in a package four levels down the treeA scanner blocks the deploy for a package nobody has heard ofYou run the transitive closure, and the fix requires a direct dependency to release firstBump the direct parent if it has released; otherwise override the resolution deliberately, with a note and an expiry, and check whether the vulnerable path is reachable at all.
The maintainer archives the repositoryNothing at all, for eighteen months, and then the runtime drops a versionYou depended on a person's continued attention and never priced itEvaluate fork versus replace versus vendor at the moment you notice, not at the moment it blocks you — the cheapest time is while nothing is broken.
A major version drops an API you useThe upgrade branch is red in forty filesThe vendor's vocabulary reached into your codeContain first, upgrade second. Extracting the owning module against the *old* version is a safe refactor; doing both at once is not (What Refactoring Actually Is).
A floating range resolves to a new minorCI fails on a commit that changed one commentThe build was never reproducible; the manifest allowed the resolver to choosePin exactly, commit the lockfile, and treat a resolution change as a code change that needs review.
Two dependencies demand conflicting versions of a thirdInstall fails, or worse, silently deduplicates to a version neither tested againstYou have inherited someone else's compatibility matrixDrop one of the two, or pin the shared child and run the affected paths under test. There is no clever resolution here — it is a decision about which dependency you value.

Direct, transitive, and who is on the hook

SCALE-SPECIFICAt four engineers, a quarterly manual read of the direct manifest is a genuinely sufficient process and a policy engine is overhead. Past roughly fifty engineers the direct manifests are no longer readable by one person and the same discipline has to become tooling — an allowlist, a bot, an ownership file. Importing the large-company version of this at four engineers produces a policy nobody enforces, which is worse than no policy because it also produces the belief that dependencies are handled.

Two words do most of the work. A direct dependency is one you named — you chose it, you can remove it, and it should have a justification. A transitive dependency is one your dependencies named. You did not choose it, you frequently cannot remove it, and you run it in production with the same privileges as your own code.

The ownership rule that follows is simple and unpopular: you are responsible for the whole tree operationally, and for the direct edges intellectually. Anything else pretends the transitive graph belongs to somebody else, and no scanner, auditor or customer agrees.

DevOps owns the machinery that makes this tractable — the registry, the pinning, the provenance and the scanning. This domain owns the decision that determines how much of it hurts: how far into your code each dependency is allowed to reach (Encapsulation Radius).

One line in the manifest, and what you actually run
speaks your vocabularythe only place that knows the vendorresolved for youarrives on their scheduleYour codeOwning module (you wrote this)Direct: chosen, justified, removableTransitive: inherited, unaudited, runningUpgrade + CVE obligation
UserLLMAgentToolDataDecisionHumanGuardrail

How to build it

Most important first.

  • Decide deliberately, once, at the point of adding. The four-minute merge is the design failure — not the package (Do We Need a Package for This?).
  • Record why. Two lines next to the manifest: what it does for us, what we would do instead, what would make us drop it (Decision Records).
  • Pin exactly and commit the lockfile, so that "it works on my machine" and "it worked yesterday" are the same statement. Floating ranges move your build without a commit (Reversible and Irreversible Decisions).
  • Contain volatile dependencies behind one module you own, and let the rest of the codebase speak your vocabulary rather than the vendor's (Anti-Corruption Layer).
  • Upgrade continuously and in small steps. Six months of skipped minors is a migration; six weeks is a bump, and the difference is entirely in when you paid (Incremental Migration).
  • Make removal a first-class operation: run an audit each quarter for packages with zero or one import and delete them. Unused dependencies are pure liability.

What the next change costs

The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.

Cost of the next change
  • The next security patch: with pinned versions, a committed lockfile and a green test suite, it is a version bump and a CI run — under an hour. Without those, it is a bisect against an unknown resolution and a manual regression pass.
  • The next major version of a directly-called library: cost is roughly the number of call sites times the number of behaviours you relied on. Behind a single owning module it is one file plus its tests; called from everywhere it is a multi-week branch that conflicts with everything.
  • Replacing the dependency entirely: cheap only if your code never learned its vocabulary. Every place a vendor type crosses one of your function signatures is a place the replacement has to be bug-for-bug compatible.
  • What does *not* get cheaper: the wrapper does nothing for a change in the dependency's semantics. If the new version rounds differently, the boundary passes it straight through, and only a test catches it.
What the recommended approach costs
  • Containment behind an owning module costs indirection forever, in exchange for a replacement that may never happen. It is the same bet as any abstraction, and it loses when the library outlives your product (The Cost of Change).
  • Exact pinning gives reproducibility and takes away automatic patch fixes, so it obliges you to run an upgrade process. Teams that pin and then do not upgrade are strictly worse off than teams that floated.
  • A justification note per dependency is real friction on a task that used to take four minutes, and some of that friction is genuinely wasted on packages that will never cause trouble.

What can go wrong

Failure modes
  • The upgrade is deferred until it is forced, at which point three major versions have to be crossed at once, under a disclosure deadline.
  • The wrapper you built to make the dependency replaceable exposes the dependency's own types, so it is a wrapper in name only and replacing it is still forty edits.
  • Automated upgrade bots open so many pull requests that the team stops reading them, and a genuinely breaking bump is merged on autopilot — the mitigation failing exactly the way the original problem did.
  • A dependency is removed from the manifest but its transitive children stay, pulled in by something else, so the vulnerability report does not change and nobody understands why.
  • The package is fine and the *version* you are stuck on is not, because an unrelated package pins a conflicting peer range. You now maintain someone else's compatibility matrix.
Dependencies, and their direction
  • Your code depends on the library's API, which depends on its maintainers' priorities — a dependency on people, not just on code, and the one nobody writes down.
  • Your build depends on a registry, a resolver and a network at exactly the moment you need to ship a fix — the registry is DevOps' machinery, but the outage is yours.
  • Your security posture depends on the whole transitive closure. The direction is one-way: they cannot break because of you, you break because of them.
Misreads
  • "So write everything ourselves." No. Cryptography, TLS, date arithmetic, compression and parsers are exactly where the ecosystem is worth its cost, and a hand-rolled version is a worse dependency with one maintainer (Build, Library, SaaS or Managed Service).
  • "Fewer dependencies is better." Count is a weak proxy. One framework that dictates your file layout is a heavier commitment than twenty leaf utilities, and the count says the opposite (What a Framework Charges).
  • "The lockfile means we are safe." A lockfile makes the build reproducible, not correct or trustworthy. It reproduces a compromised package exactly as faithfully as a good one.
  • "This is a DevOps concern." The pipeline is DevOps'. How much of your code speaks the vendor's vocabulary is a design decision made in a pull request, and no pipeline can undo it.
Smells this explains
  • duplicate-knowledge
  • shotgun-surgery

Testing it, and how it ages

What to test, and at which boundary
  • Test at the boundary you own, not the library. Asserting that a date library formats dates is testing someone else's test suite (What a Unit Is).
  • Test the behaviours you actually rely on, in your own vocabulary — "an invoice dated 31 January in Berlin renders as 31.01." — so an upgrade that changes them fails in CI rather than in production.
  • For a wrapped dependency, a small contract test over the wrapper is what makes the swap safe; without it the wrapper is an untested claim (Contract Tests).
  • Keep one test that fails if the lockfile and the manifest disagree. Reproducibility is an invariant and invariants get tested.
How this design ages
  • Dependencies do not age gracefully in place; they age by the ecosystem moving. The runtime you sit on drops a version, and a package that has not shipped in two years becomes a blocker for something unrelated.
  • Around the second or third abandoned package, teams start weighing maintainer health as heavily as features — which is the right lesson learned in the expensive way.
  • A dependency added for one feature accretes callers. What began as "we use it in the importer" becomes the codebase's date type, and the decision to adopt it was never revisited (Revisit Triggers).

Where this applies

This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.

  • GENERALThat upgrade cost scales with the number of places that know a dependency exists follows from having to find and edit each of them, so it holds across ecosystems from npm to Maven to Cargo.
  • LANGUAGE-SPECIFICThe ecosystem sets the default graph size: an npm tree of four hundred packages for one utility is normal, a Go module graph is typically an order of magnitude smaller, and Java projects tend toward fewer, larger, longer-lived dependencies. The advice to contain volatile dependencies is constant; the urgency of auditing the transitive tree is not, and reading npm-shaped advice into a Go project overstates it.
  • CONTESTEDThe strongest opposing case: wrapping dependencies to make them replaceable is speculative generality that almost never pays, because the replacement rarely happens and the wrapper reliably ends up shaped like the thing it wraps — so you get indirection, a lowest-common-denominator API, and no portability. Practitioners who hold this are right about most libraries; the disagreement is about which few are volatile enough to be exceptions, and it should be argued per dependency rather than as policy.

Where the depth lives

This domain teaches the codebase-level structure and hands the rest off.

Domains that do not exist yet
  • Testing & Reliability Engineering — whether an upgrade is a bump or a project is decided by whether your suite would notice a behaviour change in the dependency, which is a coverage question this lesson assumes rather than answers.