In a monorepo containing many internal projects, what does it mean to have a single source of truth for a shared dependency's version, and why do teams enforce it instead of letting each project pin its own version independently?
answer
- one file, one version
- catalog: protocol (pnpm)
- Bazel single version per build graph
- diamond conflict = visible, not silent
basics
~10 sOne place in the repo says "we use version X of this library" for everyone, so all teams building together use the same code and can't accidentally clash with incompatible versions.
solid answer
~50 sA single source of truth means the version of a shared internal or external dependency is declared exactly once (e.g., a root lockfile, a workspace catalog file, or a version-policy file) and every consuming project in the repo resolves to that same version rather than declaring its own pin. This matters because a monorepo's whole value proposition is that any commit can be built and tested against the current state of everything else; if project A pins library X at 1.2 and project B pins 1.5, you get two copies of X in the dependency graph (or a build failure), duplicate code shipped to runtime, and CI that can't guarantee "if it built, it's consistent." Centralizing the declaration turns a fragmentation problem into a single place to bump, test, and roll back a version for the whole repo.
go deeper
Should be able to say, in plain terms, that pinning versions in one place instead of many avoids projects silently drifting apart, even without naming a specific tool or file.
Should name a concrete mechanism (a root lockfile, a version catalog file, or an equivalent) and explain what happens without it — duplicate installs or a build failure — not just that 'it's good practice.'
Should discuss the trade-off explicitly: centralization costs team autonomy but converts silent divergence into a visible, mechanical conflict, and should be able to describe how an exception/staged-upgrade process works when a team needs to move ahead of the rest of the repo.
Should connect this to the broader architectural bet a monorepo makes — trunk-based, build-everything-together — and explain why that bet specifically requires single-version enforcement to remain tractable at scale, citing how large-scale systems (e.g., Bazel-style single-version build graphs) enforce it structurally rather than by convention.
## What a single source of truth is A **monorepo** is a single version-controlled repository that holds the source for many logically separate projects — libraries, services, frontends — that are built and tested together rather than published and consumed as independently versioned packages. The promise of that arrangement only holds if the repo can answer, unambiguously, "what version of dependency X are we running right now?" A single source of truth is the mechanism that guarantees that answer is the same no matter which project in the repo you ask. Concretely, it usually takes the shape of one file or one system: - a **root-level lockfile** (e.g., a single `pnpm-lock.yaml` or `package-lock.json` shared by every workspace package) - a **version catalog** (pnpm's `catalog:` entries in `pnpm-workspace.yaml`, or a Bazel `WORKSPACE/MODULE.bazel` dependency pin) - a **build system** that literally will not let two build targets reference two different versions of the same external artifact Every project that needs that dependency reads from that one declaration instead of writing its own version number into its own manifest. ## Why it is worth enforcing Why does this matter enough to enforce? Because the entire economic argument for a monorepo — one CI run per commit, one place to refactor a shared library and update every caller atomically, no need to publish-then-bump-then-wait — collapses the moment two projects in the same tree can silently disagree about which version of a dependency they're building against. If project A's manifest says `requests==2.28` and project B's says `requests==2.31`, a naive per-project resolution either: - **(a)** installs two copies of the library into the dependency graph, doubling binary size and creating two separate sets of bugs/CVEs to track, or - **(b)** forces the build tool to pick one arbitrarily, meaning project A is silently running code it never tested against. Either way, you've reintroduced the exact problem monorepos exist to eliminate: you can no longer trust that "tests pass on this commit" means "the whole system, built together, works." ## How the enforcement is mechanized The mechanism by which single-source-of-truth is enforced varies by ecosystem but the pattern is consistent: a **centralized manifest** is the only place a version literal is allowed to appear, and every other file references it symbolically. - **JavaScript/TypeScript monorepos using pnpm workspaces.** The `catalog:` protocol lets `package.json` files say `"react": "catalog:"` instead of `"react": "18.2.0"`, with the actual number living once in `pnpm-workspace.yaml`; a linter or CI check then rejects any `package.json` that has a literal version instead of `catalog:`. - **Bazel-based repos** (used at Google-scale monorepos). External dependencies are pinned once in `MODULE.bazel` or a `WORKSPACE` file and every `BUILD` target that depends on that library references the same resolved target label — Bazel's build graph makes it structurally impossible for two targets in the same build to link against two different versions of the same external jar, because there's only one node in the graph for that dependency. - **A Gradle multi-module repo.** A version catalog (`libs.versions.toml`) plays the same role: modules declare `libs.jackson.databind` rather than a version string, and bumping the TOML file bumps it everywhere at once. ## The trade-off The trade-off is real and worth naming honestly. Centralizing versions costs **autonomy**: a team that wants to adopt a new major version of a library early, or that is stuck needing an old version for a legacy consumer, cannot simply bump their own manifest — they either have to coordinate a repo-wide upgrade or maintain an explicit, visible exception. This is friction by design; the alternative (silent divergence) is the failure mode being prevented, not a hypothetical one. The most common real-world manifestation is a **"diamond" shape** in the dependency graph: two internal libraries both depend on a third shared library, but at different versions, and a consumer that depends on both internal libraries now has an unresolvable conflict unless one side is bumped or downgraded. Single-source-of-truth doesn't eliminate the need to resolve that conflict, but it makes it visible and mechanical (one file to change, one CI check to fail) instead of a runtime surprise discovered when someone finally tries to build everything together. ## The canonical large-scale example Google's monorepo, and its "trunk-based, everyone-at-head" model, is the canonical large-scale example: with millions of build targets sharing one dependency graph, allowing per-project version pins for widely-used libraries would make the graph combinatorially unbuildable, so the build system enforces exactly one version of each external dependency repo-wide, and upgrading it is a deliberate, tooled, often codemod-assisted event rather than something any one team can do unilaterally.
- What actually breaks at build or runtime if two internal projects in the same monorepo end up depending on different versions of the same external library?Depending on the ecosystem, you either get two copies of the library loaded at once (bloating the bundle/binary and potentially causing subtle bugs if the two copies hold separate state, like a singleton), or the build tool refuses to resolve the graph and fails outright. In strongly-typed graph-based build systems like Bazel, the second copy typically produces a symbol or type conflict that fails the build immediately rather than shipping silently.
- How do you handle a team that genuinely needs a newer major version of a shared dependency before the rest of the repo is ready to move?Most repos allow a scoped, explicit exception rather than a silent fork — e.g., a temporary second catalog entry, a labeled internal fork, or a feature-flagged parallel dependency — paired with a tracked migration ticket and CI visibility so the exception doesn't quietly become permanent. The key is that the deviation is declared and tracked, not smuggled into one project's manifest.
- Does single-source-of-truth versioning apply only to third-party packages, or also to internal libraries built in the same monorepo?It applies to both, and internal libraries are actually the easier case: since the internal library's source is in the same repo, most monorepo setups don't version it at all — every consumer just builds against the current source at HEAD, so there's no version number to desynchronize. External packages are the harder case because their source lives outside the repo and really does have discrete published versions that must be pinned somewhere.
Like a company having one official master calendar instead of every department keeping its own copy — if each team pencils in a different room for the same slot, you don't find out about the double-booking until people show up; one shared calendar makes the conflict visible before it happens.
saying these in an interview costs you the question
- thinks each project can pin its own version and 'it'll sort itself out'
- doesn't recognize duplicate library copies as a real runtime risk
- conflates single source of truth with 'we just don't upgrade anything'
- can't explain why a monorepo specifically needs this vs a polyrepo