skip to content

Does a monorepo where every service is built and deployed from HEAD violate the Reuse/Release Equivalence Principle (REP)? How should REP be applied when producers and consumers share one repository?

level: seniorimportance: should knowfreq 22%

answer

  1. REP = defined granule, not necessarily semver
  2. monorepo HEAD: commit is the version, atomic change
  3. no skew, no diamonds; producer pays migration
  4. violation = no API surface / no owner / reach-in imports
  5. external consumers ⇒ real versioned releases return

basics

~20 s

Not necessarily. REP demands that reuse happen through a defined, identifiable release. In a monorepo built at HEAD, the commit itself is the release: one version for everything, atomic changes, no pinning. It satisfies REP's intent only if boundaries, ownership and change communication still exist.

solid answer

~50 s

REP requires the reused granule to be a released granule — bounded, identified, and changed through a communicated process. A monorepo with enforced single-version consumption substitutes *one global version* (the commit/build) for many artifact versions: every consumer is on the same code, changes are atomic across producer and consumers, and there is no diamond-conflict problem or upgrade backlog. That is a legitimate answer to REP, not a violation — provided the component still has a real boundary (declared public API, visibility rules, ownership) and change is communicated. What genuinely violates REP is unbounded reach: any file importing any other, no owner, no API surface, breakage discovered at runtime. The trade-off: the monorepo shifts cost from consumers (upgrade toil) to producers (you break it, you fix all call sites) and requires heavy tooling — build graph, CI at scale, code ownership. The moment a consumer sits outside the repo, you need real versioned releases again.

code

text · 8 lines
text
Monorepo, REP-compliant boundary (build target as the release granule):
  //libs/payments:client        visibility = ["//services/checkout", "//services/billing"]
  //libs/payments:internal      visibility = ["//libs/payments:__subpackages__"]
  OWNERS: payments-team    CHANGELOG.md: behaviour changes + deprecations

Monorepo, REP violation (no granule at all):
  //services/checkout imports libs/payments/internal/RetryLoop directly
  # no declared edge, no owner, no API promise -> producer can't change anything safely

go deeper

for a junior

Say REP is about having a clear, identifiable unit you depend on; in a monorepo the commit plays the role of the version, so it isn't automatically a violation.

for a middle

Contrast the two models concretely — pinned versions with skew and diamonds versus HEAD with atomic change — and name what still must exist in a monorepo: API surface, ownership, changelog.

for a senior

Weigh the trade (consumer upgrade toil vs producer migration cost), describe tooling requirements (visibility rules, dependency linting, large-scale refactoring, CI at scale), and cover the hybrid case where external consumers force real versioned releases.

for a principal

Treat it as an organisational strategy choice tied to team topology, CI budget, external commitments and regulatory needs; define the public-API and deprecation policy, and state the conditions under which you would move between models.

## Restating REP precisely **REP**: *the granule of reuse is the granule of release.* The intent is that reuse happens through a **defined, identifiable, communicated unit** — you can say exactly what you depend on, exactly which state of it, and you learn when that state changes. Version numbers in a package registry are the *usual* implementation of that intent, not the intent itself. ## Two coherent models **Model A — independently versioned artifacts (multirepo, or monorepo publishing packages).** Each component is built and published with its own semver line. Consumers pin versions and upgrade deliberately. - Consumers control timing; can stay on an old version. - Producers can change without immediately fixing every caller (breaking changes are shipped as MAJOR and migrated over time). - Costs: **version skew** (many consumers on many versions of you), **diamond conflicts** (two paths need incompatible versions of a shared dependency), backported fixes, long tail of stale consumers, and slow propagation of security fixes. **Model B — single-version monorepo built at HEAD.** All code lives in one repository; consumers depend on the component at HEAD; a commit is the unit of release. - The **commit SHA is the version** — a real, immutable identifier for the whole world. - Changes are **atomic**: a producer change and all consumer updates land in one commit, so there is no intermediate broken state, no skew, and no diamond problem (there is exactly one version of everything). - Fixes propagate instantly to everyone. - Costs: producers bear migration cost ("you break it, you fix it" across all call sites), CI must build/test the affected slice of a huge graph, large-scale refactoring tooling is mandatory, and *nobody can opt out* of a change — a bad change hits everyone at once. Model B does not abandon REP; it collapses many release granules into one global granule. The Google-style single-version policy is the canonical example. What it *does* abandon is per-component independent versioning — deliberately, in exchange for eliminating skew. ## When a monorepo genuinely violates REP The repository layout is not what matters; the **boundary and the process** are. Red flags: - **No declared public API.** Consumers import internal classes/files directly, so the producer cannot change anything safely and there is no surface to promise stability on. Fix: visibility rules, module/export declarations, enforced dependency rules (architecture tests, build-visibility lists, dependency-cruiser-style checks). - **No ownership.** Nobody is accountable for the shared code, so nobody can approve changes or run a migration. Fix: code-owner files and a component charter. - **No change communication.** Even with atomic commits, consumers need to learn about behaviour changes, deprecations and new capabilities. Fix: changelog per component, deprecation annotations with removal dates, announcements for behavioural changes. - **Ad-hoc reach-in reuse across team boundaries**, e.g. Team A's build compiles Team B's source directory with no declared dependency edge. This is a REP violation in *any* repo layout: reuse with no identified granule. ## Hybrid: monorepo that publishes Many organisations run a monorepo internally *and* publish some components externally. Then you get both models at once: internal consumers on HEAD, external consumers on semver artifacts. Consequences to plan for: - The **public API surface must be explicitly delimited** — what's covered by the compatibility promise versus internal (visible internally, unstable). - **Release cutting** becomes a separate act from committing: tag, build, publish, changelog. - You now support **old versions** for external consumers — backports and support windows — even though internal consumers never see them. - "Works at HEAD" is no longer proof for external consumers; you need compatibility testing against supported released versions. ## Applying REP's *contents* rule inside a monorepo Even at HEAD, the theme/cohesion argument survives in modified form. Without version churn, the pressure changes shape: - **Build and test blast radius** replaces version churn: a themeless mega-component means every change rebuilds and retests everything that touches it, and every consumer's CI is affected. - **Dependency footprint** still matters: depending on a fat target pulls its whole transitive closure into your binary/build. - **Ownership and review load** still argue for coherent, owned components. So you still split by theme and by consumer usage clusters (Common Reuse Principle) and by co-change (Common Closure Principle) — you just do it with build targets and visibility rules rather than published versions. ## Decision guide | Situation | Lean toward | |---|---| | All consumers inside one org, one repo, unified CI, strong refactoring tooling | Single-version HEAD (Model B) | | Consumers outside the org, or independent deployment schedules you don't control | Versioned artifacts (Model A) | | Regulated consumers needing pinned, auditable dependency versions | Versioned artifacts | | Many teams, weak CI capacity, cannot rebuild the world per change | Versioned artifacts, or monorepo publishing packages | | Both internal and external consumers | Hybrid, with an explicit public-API surface | ## Bottom line REP is not "use a package registry". It is: *reuse must go through a bounded, identified, owned, change-communicated unit.* A monorepo can satisfy that with commits, build targets and ownership rules; a multirepo can violate it with copy-paste and floating versions. Judge the process, not the folder structure.

  • A single-version monorepo removes diamond dependency conflicts. What does it cost, and who pays?
    The producer pays. Because everyone is on HEAD, a breaking change must be accompanied by fixes to every call site in the same atomic commit — so large refactors require code-modification tooling and reviewer coordination across teams. It also demands CI that can build and test the affected slice of a very large graph, and it removes the consumer's ability to opt out or delay: a bad change reaches everyone at once, so rollback speed and pre-submit testing become critical.
  • Your monorepo component also has external consumers on published versions. What must you add?
    An explicit public-API surface delimiting what carries the compatibility promise versus what is internal-only; a release process separate from committing (tag, build, publish, changelog); semantic versioning discipline on that surface; support windows and backport policy for released versions still in use; and compatibility testing against supported released versions, since 'green at HEAD' no longer proves anything for someone on 3.2.x.
  • How do you keep a shared monorepo library from being reached into, given there are no package boundaries to enforce?
    Enforce boundaries in tooling rather than trusting convention: build-system visibility rules or export declarations that whitelist who may depend on a target, architecture tests or dependency-graph linters in CI that fail on disallowed edges, code-owner rules requiring the owning team's review, and language-level visibility (internal/private modules) where available. Combine with a written charter stating what the public surface is.

Independently versioned artifacts are like a city where every building renovates on its own schedule — nobody is disrupted, but the street ends up in twelve different decades. A single-version monorepo is a planned campus that renovates everything in one coordinated shutdown: no mismatch anywhere, but the whole campus feels every change, and the renovator must fix every room they touch.

saying these in an interview costs you the question

  • "Monorepos violate REP by definition" — REP is about a defined release granule, and a commit under a single-version policy is one.
  • "A monorepo means no versioning is needed at all" — the commit is still the version, and any external consumer still needs published, immutable releases.
  • "Same repo means no boundaries needed" — without a declared API surface, ownership and visibility rules, the producer can never change anything safely.
  • "Multirepo automatically satisfies REP" — copy-pasted code, source-path dependencies or floating `latest` versions violate it regardless of repo count.
  • "Atomic commits remove the need for changelogs" — consumers still need to learn about behaviour changes and deprecations even when they can't be on an old version.
  • "Single-version policy is free" — it shifts migration cost onto producers and demands serious CI and refactoring-tooling investment.

context