skip to content

A team refactors a shared internal library's public function signature and needs to update 40 internal callers across a dozen projects at the same time. How does doing this inside a monorepo, in one atomic commit, differ from doing the equivalent in a multi-repo (polyrepo) setup where the library is published as a separately versioned package?

level: middleimportance: must knowfreq 70%

answer

  1. one commit, one CI run, whole graph
  2. no manifest version bump per consumer
  3. LSC / codemod tooling
  4. version skew window in polyrepo

basics

~20 s

In a monorepo you change the library and all 40 callers in one commit that either fully passes or fully fails together. In separate repos, you'd publish a new library version first, then update each caller's dependency one at a time, with a gap where they're out of sync.

solid answer

~50 s

In a monorepo, the library and every consumer live under one version-control root, so a single commit/PR can rewrite the library's signature and every one of its 40 call sites, and CI runs the whole graph against that one commit — either everything builds and passes together, or the change doesn't land. There's no intermediate state where some callers use the old signature and some use the new one. In a polyrepo, the library is a separately versioned, separately published artifact; you must publish a new version first, then go update each of the 12 consuming repos' dependency declarations one at a time (or via automated PRs), each on its own review/merge/deploy cycle. Between the publish and the last consumer's upgrade, the repos are running mismatched versions — a real but temporary window of version skew that the monorepo model structurally avoids.

go deeper

for a junior

Should grasp that one commit changing everything at once means you don't end up with some parts of the codebase using the old version and some the new.

for a middle

Should be able to contrast the mechanics precisely: no per-consumer manifest version bump in a monorepo vs. a publish-then-upgrade-each-consumer sequence in polyrepo, and name version skew as the polyrepo-specific risk.

for a senior

Should discuss the CI/build-graph cost atomicity imposes at scale and know at least one mitigation (affected-graph analysis, remote caching, or LSC/codemod tooling), plus recognize that deliberate staged rollout is different from accidental skew.

for a principal

Should reason about organizational trade-offs — larger blast radius per change, need for pre-submit gating and ownership/approval processes on foundational libraries, and how large-scale-change tooling is a load-bearing part of making monorepo atomicity viable at hundreds-of-thousands-of-targets scale.

## What atomicity means here Atomicity here means the same thing it means in a **database transaction**: either the entire set of changes lands together, or none of it does — there is no state where an observer can see the change half-applied. In a monorepo, that property comes almost for free from the fact that the library and every one of its consumers are files in the same working tree, tracked by the same version control history. - A refactor that changes a function's signature is expressed as a **single commit** (or a single PR made of several commits, but merged atomically) that edits the library's source and rewrites every call site in the same changeset. - The **CI system** that gates the merge builds and tests the entire affected subgraph — the library, plus every project that transitively depends on it — against that one commit. - If any caller was missed, or the new signature breaks a test somewhere in project #7 of 12, the whole change fails to merge. There is no window, ever, where the repository's `HEAD` contains the new library signature alongside a caller still written for the old one, because the commit that would create that state simply cannot pass CI and cannot land. ## The polyrepo contrast Contrast that with a polyrepo setup, where the shared library is packaged and published independently (to an internal or public registry) with its own semantic version. Here, "the same change" is unavoidably split into multiple, separately-merged, separately-deployed steps: 1. **First**, the library repo publishes a new version (say `3.0.0`, a major bump signaling the breaking signature change). 2. **Second**, each of the 12 consuming repos must independently bump their dependency declaration from `^2.x` to `3.0.0`, update their own call sites to match, run their own CI, get their own review, and merge and deploy on their own schedule. Nothing forces those 12 updates to happen simultaneously — in practice they land over hours, days, or (for a slow-moving repo) months. During that interval, the library exists at two versions across the organization: some services running against 2.x, some against 3.0.0. This is **version skew**, and it's not a bug in the process, it's an inherent property of independently-versioned, independently-deployed components. ## Why the monorepo path works The mechanism that makes the monorepo path possible is that the consuming projects don't declare a version of the internal library at all — they simply import its current source at whatever commit `HEAD` happens to be. There's no manifest entry like `"internal-lib": "3.0.0"` to update project-by-project; the "version" is implicitly "whatever's checked in right now," and that's identical for every consumer by construction. This is why monorepo advocates describe the internal dependency graph as being pinned to a **single point in time** rather than to a version number. ## What it costs The trade-off is where CI cost and blast radius go up in exchange. Every commit to a widely-depended-on library must build and test not just itself but every downstream consumer, which can mean minutes-to-hours of CI time on a large graph, and tooling (build graph analysis, remote caching, selective test execution) becomes load-bearing infrastructure rather than a nice-to-have. It also means a single bad commit to a core library can, in principle, block or break many teams at once — the atomicity that prevents skew also means the "unit of failure" is larger. Large monorepo shops mitigate this with: - **pre-submit checks** that build the full affected graph before allowing a merge (so breakage is caught before landing, not after), - **automated large-scale-change (LSC) tooling** that can generate and land the caller-side edits across hundreds of call sites as part of the same changelist, - and sometimes **staged rollout mechanisms** for especially large or risky refactors. ## Where it shows up in practice A concrete real-world example is Google's monorepo practice around "large-scale changes": when a widely-used API's signature changes, the change author (often assisted by tooling that mechanically rewrites call sites, i.e. a codemod) produces a single changelist that touches the library and every caller across the company's codebase, and it's tested and submitted as one atomic unit — sometimes broken into several dependent changelists for very large blast radii, but still coordinated and landed as a unit rather than left to each team to migrate independently over time. The failure mode this avoids in practice is the classic problem of polyrepo ecosystems: a core library gets a breaking fix, most consumers upgrade promptly, but a handful never do, and much later someone discovers several services are still running a version with a known security vulnerability because nothing forced them to move together.

  • What breaks down about atomic cross-project commits when the monorepo gets very large, say hundreds of thousands of build targets?
    The CI cost of testing the full transitive closure of every commit to a foundational library becomes enormous, so large-scale monorepos invest heavily in build graph analysis to only rebuild/retest what's actually affected, plus remote caching and distributed test execution. Without that investment, atomicity is theoretically preserved but practically too slow to be usable.
  • Is version skew always bad, or are there legitimate reasons a team would want a polyrepo-style staged rollout even inside a monorepo?
    Staged rollout is legitimate when a change is risky enough that you want a canary period — feature flags, gradual service-by-service enablement, or a deliberate deprecation window for a large API change are all ways teams intentionally introduce controlled, temporary skew even in a monorepo, rather than forcing one atomic flip. The difference from polyrepo skew is that this staging is a deliberate, visible, time-boxed choice, not an emergent side effect of independent release cycles.
  • How do large-scale change tools actually manage to touch hundreds of call sites in one changelist without a human manually editing each one?
    They typically use structural/AST-aware code search-and-rewrite (a codemod) that pattern-matches the old call signature across the whole codebase and mechanically rewrites it to the new form, often paired with a review/approval step per affected project owner even though the change lands as one commit. This scales what would otherwise be dozens or hundreds of manual PRs into one tooled, largely automated changelist.

It's like renovating a single building's plumbing versus coordinating 12 separate homeowners each connected to the same water main — in the building, one crew can shut off and replace the pipe and reconnect every unit in one afternoon; across 12 independent homes, each owner has to schedule their own plumber, and for a while some houses have the old pipe and some have the new one.

saying these in an interview costs you the question

  • believes monorepos still require bumping an internal package version per consumer
  • can't explain why polyrepo upgrades create a temporal skew window
  • thinks atomic commit means 'fast', not 'all-or-nothing'
  • no awareness that testing the full affected graph has a real CI cost

context