THIS EXPLANATION
THE ROOM
CDA·71 Computing, Data & AI 6 MIN · 8 STATIONS

Dependency conflicts

A Socratic walk-through of dependency conflicts — reasoned out one step at a time, not lectured.

abcdefgh
a

The question we started with

THE QUESTION #

Why can upgrading one library break a program that never used it in the first place?

You upgrade a charting library. The build fails in the authentication code, which does not draw charts, has not been edited in a year, and does not mention the charting library anywhere. The natural reading is that something is broken. But nothing is broken — and the reason is worth chasing, because it explains why this happens in every ecosystem that has ever tried to share code, and why the fix is different in each one.

b

Reasoning it through

REASONING #

Follow what the two libraries have in common rather than what they do. Your program names two dependencies directly; each of those names further dependencies of its own, and those name more. The set you actually install is not a list, it is a graph, and most of its nodes are packages you have never heard of. Now suppose the charting library and the authentication library both, several levels down, depend on the same small utility package. That is the diamond: two paths from your program that meet again at a shared node.

Ask the question the installer has to answer. Which version of that shared package gets installed? In Python, on a Java classpath, in a Ruby bundle — in any ecosystem where an imported name resolves to exactly one thing — there is a single slot. Charting version 3 demands utility 2.x; authentication was written against utility 1.x and calls a function removed in 2.0. Both constraints must be satisfied against one slot, and they cannot be. The installer either refuses, or picks one and hands you a program that assembles fine and fails when the losing library reaches for something no longer there. Nothing was broken. Two requirements simply became unsatisfiable at the same time, and the upgrade you performed was merely what made them so.

So why not install both versions? Some ecosystems do exactly that. Node's package manager nests copies, so each dependent gets its own; Cargo allows semver-incompatible major versions to coexist in one binary. This genuinely dissolves the diamond, and it tells us the shared slot was the cause rather than any defect in the libraries.

But it does not dissolve everything, which is the more interesting half. Two copies coexist peacefully only while nothing crosses between them. The moment an object created by utility 1.x is passed to a library holding utility 2.x, the receiving code sees a type that has the same name and is not the same type, and rejects it. Anything that was supposed to be global — a connection pool, a logging registry, a lock — silently becomes two, each behaving as though it were alone. Duplication converts a loud resolution failure into a quiet runtime one. The real invariant, then, is not "one version per program" but "one version per boundary that values cross".

Two further things make this harder than bookkeeping. First, the resolver's job is not merely fiddly, it is combinatorially hard: choosing versions to satisfy a graph of ranges is a Boolean satisfiability problem, shown to be NP-complete for Debian-style dependencies, which is why modern installers use real SAT or PubGrub solvers rather than trying combinations.

Second, and more consequential, the constraints being solved are promises rather than facts. Semantic versioning is a convention — major for breaking changes, minor for additions, patch for fixes — and writing a range like ^1.2 means trusting every future 1.x to keep behaving. Nothing verifies it. Hyrum's law observes that with enough users, every observable behaviour of a system is depended on by somebody, so even a bug fix breaks someone. The consequence cuts both ways: some conflicts are real incompatibilities, and a great many are two maintainers' differing guesses about compatibility, one pinning too tightly and thereby forbidding a version that would have worked perfectly.

c

The analogy

THE ANALOGY #
THE FIGURE

Picture a shared workshop with one bench vice. Every job's instructions were written against a particular vice — its jaw width, its handle. A new job arrives needing a bigger model, so the vice is replaced, and now a completely unrelated job that has sat untouched for months no longer clamps, because its instructions assumed the old jaws. Nobody's instructions are wrong. There is one vice and two incompatible assumptions about it.

WHERE IT BREAKS DOWN

A workshop can simply buy a second vice and stand it beside the first, and that is exactly what the ecosystems that allow duplicate versions do — but the analogy hides why that is not always enough, because two vices only work while no half-clamped workpiece has to be carried from one bench to the other, and that handover is precisely the case where two copies of a library fail.

d

Clarifying the model

THE MODEL #

The misconception to dislodge is that a dependency conflict is a bug report. It is a constraint system reporting that it has no solution — more like an overbooked calendar than a crash. That reframing changes what a fix looks like. You are not looking for the broken code; you are looking for the tightest constraint, and asking whether it is honest. Often the resolution is to relax a range, or to upgrade the other library so both can meet at a common version, rather than to revert the one you touched.

It also explains the standard escape hatches, each of which buys the same thing by a different route: vendoring a private copy, shading or relocating a library into a renamed namespace, isolating a component behind a process boundary so it carries its own dependency graph. All three restore per-consumer versions, and all three pay the same price — more copies to patch when a vulnerability lands in one of them, and the type-identity hazard at every boundary values cross.

One thing worth stating as contested rather than settled: whether ecosystems should permit duplicate versions at all is a genuine design disagreement, not a solved question. Single-version ecosystems get conflicts early and loudly, and a program that assembles is coherent. Multi-version ecosystems almost never fail to resolve, and pay with bloat, duplicated state, and failures that surface at boundaries instead of at install time. Neither has found a way to have both.

e

A picture of it

THE PICTURE #
Dependency conflicts
Dependency conflicts This repurposes the class-diagram family, so each box is a package rather than a class and each labelled arrow is a version-constrained dependency. Start at the top and follow both arrows down: the two paths leave your application independently and meet again at the bottom, which is the diamond. The dashed line joins the two utility versions that cannot both occupy the single installable slot -- and note that neither of them appears in anything you wrote, which is why the failure surfaces in code you never edited. {"generator":"[email protected]","source":"../Socrates/.diagram-cache/_src/dependency-conflicts.md","sourceIndex":1,"sourceLine":4,"sourceHash":"d741a24b03629c26b625a1a13f4f0142ee9a2a14122deb54f92770fa58ebdc61","diagramType":"class","layoutVariant":"source","repairedDuplicateIds":[],"motion":"entrance-with-reduced-motion-fallback","presentation":"editorial","attempt":1,"viewBox":{"x":0,"y":0,"width":720,"height":757},"qa":{"passed":true,"findings":[]}} you upgraded this untouched for a year requires 2.x requires 1.x same name, one slot Your application Charting library 3.0 Authentication library Utility 1.4 Utility 2.1

How to readThis repurposes the class-diagram family, so each box is a package rather than a class and each labelled arrow is a version-constrained dependency. Start at the top and follow both arrows down: the two paths leave your application independently and meet again at the bottom, which is the diamond. The dashed line joins the two utility versions that cannot both occupy the single installable slot — and note that neither of them appears in anything you wrote, which is why the failure surfaces in code you never edited.

f

What became clearer

WHAT CLEARED #
WHAT CLEARED

The breakage travels through a package you never named. Upgrading one library changes its demands on something shared far below, and an unrelated library's older demand on that same shared thing becomes impossible to satisfy simultaneously. The conflict is a property of the graph, not of any node in it — which is why the question to ask is never "what did the upgrade break" but "which two constraints now overlap on one slot, and is either of them stricter than the truth".

g

Where to go next

ONWARD #
  • How lockfiles differ from constraint ranges, and why committing one changes what "it works on my machine" means.
  • Why a language's module system, more than its package manager, decides whether duplicate versions are even expressible.
h

Key terms

TERMS #
TermWhat it means
Transitive dependencya package you depend on only because something you depend on depends on it.
Diamond dependencytwo dependency paths from one program that converge on the same shared package at different versions.
Semantic versioningthe convention that version numbers signal the kind of change, so ranges can express what a dependent will tolerate.
Vendoring / shadingbundling or renaming a private copy of a dependency so it cannot collide with another consumer's copy.
Hyrum's lawthe observation that with enough users, every observable behaviour of an interface becomes something someone relies on.

Every term the collection defines is gathered in the glossary.

Nearby on the shelf

4