skip to content

Your team publishes a shared library called `common-utils` that contains HTTP retry helpers, date formatting, CSV parsing, and a feature-flag client, all versioned together. Judged against the Reuse/Release Equivalence Principle (REP), what is wrong and what would you change?

level: middleimportance: must knowfreq 34%

answer

  1. one version number = one shared promise
  2. no theme → churn + false MAJOR bumps
  3. split by consumer usage clusters
  4. facade the old coordinate, migrate, deprecate
  5. charter + owner + changelog per component

basics

~20 s

The component has no coherent theme: nobody reuses all four things together, yet everyone shares one version. Any change forces unrelated consumers to see new versions and possibly breaking bumps. Split it into themed, separately versioned components with their own release notes.

solid answer

~50 s

REP requires the released unit and the reused unit to be the same, which implies the contents share a theme, an audience and a release cadence. `common-utils` fails that: a CSV-parser consumer is dragged through every retry-helper release, and a breaking change in the feature-flag client forces a MAJOR bump on people who never call it. Symptoms: constant version churn, upgrades nobody wants, dependency conflicts because everything transitively pulls the same fat artifact, and reluctance to change anything because "it's in common". Remedy: identify the actual reuse clusters from consumer usage, split into themed components (`http-retry`, `feature-flags`, `csv`), each with its own name, semver line, changelog and owner; keep `common-utils` as an empty or deprecated aggregate for one migration window, then retire it. Migrate incrementally, republishing old coordinates as thin facades so consumers can move version by version.

code

text · 9 lines
text
Before (REP smell): everyone depends on one fat, themeless artifact
  billing-svc  -> common-utils 9.3.0   (uses only CSV)
  gateway-svc  -> common-utils 9.3.0   (uses only retry)
  # flag-client breaking change -> common-utils 10.0.0 -> BOTH must react

After (REP-aligned): themed, independently versioned components
  billing-svc  -> csv 1.2.0
  gateway-svc  -> http-retry 3.1.0
  common-utils 9.4.0  = deprecated facade re-exporting both (migration window)

go deeper

for a junior

Say the contents have no common theme, so one version number is shared by unrelated code; propose splitting into libraries that each do one thing, each with its own version.

for a middle

Add the concrete consequences — churn, false MAJOR bumps, dependency footprint — and outline the extract-behind-a-facade migration with deprecation.

for a senior

Diagnose with evidence (consumer usage matrix, co-change history, dependency footprint), define semver's public-API surface, plan incremental migration and adoption tracking, and state where you would not split.

for a principal

Treat it as platform policy: component charters and owners, release/deprecation/support standards, registry and tooling investment, guardrails to prevent regression, and the deliberate exception where a monorepo single-version policy changes the trade-off.

## Restating the principle **REP (Reuse/Release Equivalence Principle)** — *the granule of reuse is the granule of release.* A **component** is a deployable/distributable unit (jar, npm package, wheel, module). REP says whatever people reuse must be released as such a unit: named, bounded, versioned, published, with release notes. The often-forgotten second half is the implication: because a release carries **one version number for everything inside**, the contents must be things people reasonably take *together*. ## Why a grab-bag component violates that A version number is a single promise about a whole artifact. Put unrelated things behind it and you get: 1. **Shared churn.** Every fix anywhere produces a new version everyone sees. Consumers of the quiet parts either upgrade constantly for nothing, or stop upgrading at all — and then miss the fixes that *do* matter (including security fixes). 2. **Shared breakage.** A backward-incompatible change to the feature-flag client is a MAJOR bump on the artifact. Under semantic versioning, MAJOR is a signal to *every* consumer that migration attention is required, including consumers who only parse CSVs. 3. **Coupled dependency closure.** The fat artifact drags in the union of all its third-party dependencies. A CSV consumer inherits the HTTP client library, the flag SDK's transport, etc. — bigger builds, more CVE surface, more chances of a **diamond conflict** (two paths requiring incompatible versions of the same transitive dependency). 4. **Ownership vacuum.** "Common" typically means nobody owns it. Reviews get lax, the theme erodes further, and nobody dares change anything for fear of unknown consumers. 5. **Coordination tax.** Anyone needing a one-line CSV fix must get a release of the whole artifact cut and adopted. ## Diagnosing it properly (don't split on intuition) Use evidence, not taste. Concretely: - **Consumer usage matrix.** For each consumer, which classes/functions does it actually import? Cluster consumers by the set they touch. Clusters that never overlap are separate components; that's a direct read of the reuse granule. - **Change history.** Which files change in the same commits/releases? Co-changing files want to be together (that's the Common Closure Principle's concern); files that never co-change and never co-import are prime split candidates. - **Dependency footprint.** If one subset is the only reason a heavy third-party dependency is present, that subset is a natural component boundary. ## The remediation, step by step 1. **Name the themes.** e.g. `http-retry`, `feature-flags`, `csv`, `time-format`. Each gets an owner, a repo/build target, a semver line and a `CHANGELOG`. 2. **Extract without breaking anyone.** Move code into the new components; make the old `common-utils` version depend on them and re-export the same public API (a **facade**/aggregator release). Consumers keep compiling on the old coordinate. 3. **Migrate consumers incrementally**, one at a time, swapping the fat dependency for the specific ones. Track adoption. 4. **Deprecate and retire.** Mark the aggregate deprecated with a stated end-of-support date; when adoption reaches ~100%, stop publishing it. 5. **Prevent regression.** Add a rule — review checklist, architecture test, or lint — that new code entering a shared component must fit its stated theme, plus a written charter per component ("what belongs here / what doesn't"). ## What "release process" means concretely (REP's often-skipped half) A REP-compliant component needs more than a build: - **Immutable versions.** Never republish different bytes under an existing version. - **A version policy.** Usually semver, with a documented definition of what counts as the *public API* (what's covered by the compatibility promise) versus internal. - **Release notes per version**, written for consumers: what's added, fixed, broken, and how to migrate. - **Ownership and cadence.** Who cuts releases, on what trigger, what the review/test bar is. - **Deprecation policy.** How long an API stays after being marked deprecated, and how consumers are told. - **A registry** consumers can resolve from, including old versions (so pinning actually works). ## Trade-offs and edge cases - **Don't over-split.** Splitting to per-class components creates a large dependency graph, more diamond conflicts, and multi-release coordination for a single logical change. Splitting `common-utils` into 4 themed components is usually right; into 40 is usually not. - **Genuinely tiny, universally-used, ultra-stable code** (e.g. a couple of null-safe string helpers used by literally everyone and changed once a year) can legitimately live in one small `core` component — the release churn REP worries about is negligible when the code is frozen. Judge by churn and audience overlap, not by aesthetics. - **Early-stage code.** Before a component is stable and widely reused, tight release discipline can be premature; Martin's own maturity argument is that components start life optimised for developability (ease of change) and shift toward reusability as consumers appear. That's a *stage*, not an excuse forever. - **Monorepo with enforced single-version consumption** changes the calculus: if all consumers are built and released together at head, the version-churn argument weakens — but the theme/ownership and dependency-footprint arguments remain.

  • How do you split a widely-used shared library without a flag-day migration that breaks every consumer at once?
    Extract the code into new themed components first, then republish the old coordinate as a thin facade that depends on them and re-exports the identical public API. Existing consumers compile and run unchanged. Migrate consumers one at a time to the specific components, track adoption, mark the facade deprecated with a published end-of-support date, and only stop publishing it once adoption is complete.
  • After splitting, how do you stop the new components from silently degenerating back into grab-bags?
    Give each a written charter (what belongs, what doesn't) and a named owner, and enforce it in review. Automate what you can: dependency rules or architecture tests that forbid disallowed dependencies, and API-surface checks. Watch the metrics that signal drift — unrelated co-changes, release frequency spikes, and consumers importing only a small disjoint slice.
  • Doesn't splitting one component into four just move the pain into dependency management?
    It moves some pain there, yes — more coordinates, more upgrade decisions, more diamond-conflict potential. The trade is that each consumer now only feels changes it has a stake in, and the dependency closure shrinks. Splitting is justified when consumer usage clusters are genuinely disjoint and the churn is real; if usage overlaps heavily, keeping them together is the better call.

common-utils is a magazine subscription where sports, recipes and stock listings ship in one bundle. Every reader pays for and receives all of it, and a redesign of the recipe section reprints the whole issue. Splitting into separate titles lets each reader subscribe to — and be interrupted by — only what they read.

saying these in an interview costs you the question

  • "It's fine because the unused code doesn't hurt anyone" — it does: shared version churn, false MAJOR bumps, larger dependency closure and CVE surface.
  • "Just never bump the major version" — hiding breaking changes under MINOR/PATCH breaks consumers silently and destroys the value of the version signal.
  • "Split every class into its own package" — over-splitting trades one problem for dependency-graph explosion and multi-release coordination.
  • "We'll do a big-bang migration next quarter" — flag-day cutovers on a shared library are how these efforts stall; use a facade and migrate incrementally.
  • "Add a changelog and we're compliant" — a changelog on a themeless artifact still leaves everyone sharing churn and breakage.
  • "Nobody owns common-utils, that's why it's shared" — unowned shared code is the mechanism by which the theme erodes.

context