skip to content

A shared module measures Instability I = 0.1 and Abstractness A = 0.05, and it is modified in most releases. Walk through how you would move it back toward the main sequence.

level: seniorimportance: should knowfreq 24%

answer

  1. A + I − 1 = −0.85 ⇒ deep in the zone of pain
  2. Churn × fan-in = actual cost, refactor by that ranking
  3. Split first (CCP/CRP), then invert
  4. Extract interfaces to a NEW component: api→(0,1), impl→(1,0)
  5. Interfaces beside their impls raise A but invert nothing

basics

~20 s

It is stable and concrete — many modules depend on it, it has no interfaces, and it keeps changing. Extract its interfaces into a new contract module that dependents import, leave the concrete code in an implementation module nobody depends on, and split it by reason to change.

solid answer

~60 s

Signed distance A + I − 1 = −0.85 puts it deep in the zone of pain, and high churn makes that expensive: every change recompiles, retests, and redeploys a large fan-in. Sequence: (1) confirm real volatility and fan-in from version control and the dependency graph — don't refactor a frozen module; (2) split the module by *reason to change* (Common Closure Principle) and by *who uses what* (Common Reuse Principle), because a heavily-depended-on grab-bag is usually several components; (3) for each part that must stay shared, apply Dependency Inversion at component scale — extract the interfaces into a **new** contract component that consumers depend on, and leave implementations in a component that implements the contract. The contract component moves to (I≈0, A≈1), the implementation component loses its afferent coupling and moves to (I≈1, A≈0); both land on the main sequence and the edge is inverted. (4) Re-measure and guard the improvement with an architecture test. Critically, adding interfaces *inside* the original module raises A on paper but inverts nothing.

code

pseudocode · 16 lines
pseudocode
// BEFORE: one component, I=0.1 A=0.05, everyone imports the concrete class
component shared-core {
  class PricingCalculator { ... }   // volatile, edited every release
  class TaxTable { ... }
}
// consumer-a, consumer-b, consumer-c  ->  import shared-core.PricingCalculator

// AFTER: two components, dependency inverted
component core-api {                // I≈0, A≈1  -> on the main sequence
  interface PricingCalculator { price(order): Money }
}
component core-impl {               // I≈1, A≈0  -> on the main sequence
  class DefaultPricingCalculator implements core-api.PricingCalculator { ... }
}
// consumers import core-api only; composition root binds impl -> api
// changing core-impl no longer recompiles or redeploys any consumer

go deeper

for a junior

Identify the corner (stable + concrete = zone of pain) and say the direction of the fix: pull interfaces out so dependents stop touching concrete code.

for a middle

Give the two-component before/after shape and explain why each new component lands on the line, plus how the binding happens (DI/composition root).

for a senior

Lead with measurement (churn × fan-in), split by CCP/CRP before abstracting, then invert; call out the same-component-interfaces anti-pattern and enforce the result with an architecture test.

for a principal

Treat it as coupling between teams and release trains, not just packages: sequence the refactor by blast radius, consider duplication or versioned contracts as legitimate alternatives, define an ownership model for the contract component, and make D a tracked trend rather than a gate.

### Reading the numbers first - **I = 0.1** = Ce/(Ce+Ca) is near zero ⇒ afferent coupling `Ca` dominates: **many components depend on it**, it depends on almost nothing. Maximally *stable*, meaning maximally **expensive to change**. - **A = 0.05** ⇒ virtually no interfaces or abstract types: no seam through which dependents can vary behaviour. - **Signed distance** `A + I − 1 = −0.85` ⇒ deep below the main sequence (`A + I = 1`), heading for the origin: the **zone of pain**. - **Modified in most releases** ⇒ the volatility that turns a bad coordinate into real cost. (Without churn this could be a perfectly acceptable frozen utility — the classic `String`-class case.) The pathology is **rigidity**: to change behaviour you must edit code that a large fan-in depends on, forcing rebuild, retest, redeploy and cross-team release coordination every time. ### Step 1 — Measure before you move - Pull **churn** per path from version control (touches per release) and cross it with `Ca`. `Ca × churn` gives you the true blast radius and ranks the work. - Get the **actual dependency graph**, per dependent: which types does each consumer really use? Grab-bags usually show disjoint usage clusters. - Check for **cycles** (Acyclic Dependencies Principle). A zone-of-pain module inside a cycle needs the cycle broken first — often by the same interface extraction. ### Step 2 — Split by reasons to change and by usage Before reaching for abstraction, ask whether this is even one component: - **CCP (Common Closure Principle)** — gather into a component the things that change **for the same reason at the same time**. If `shared-core` changes for tax rules, for logging format, and for date parsing, those are three components. Splitting alone drops each fragment's fan-in and stops unrelated changes from rippling. - **CRP (Common Reuse Principle)** — don't force dependents to depend on things they don't use. If module X uses only the date helpers, it should not be recompiled when the tax rules change. Often Steps 2 alone moves each fragment much closer to the line, because both `Ca` and `Nc` shrink. ### Step 3 — Invert at component scale (the DIP move) For whatever must remain widely shared and volatile: **Before** ``` [consumer-a] ─┐ [consumer-b] ─┼──▶ [shared-core] I≈0.1 A≈0.05 (all concrete) [consumer-c] ─┘ ``` **After** ``` [consumer-a] ─┐ [consumer-b] ─┼──▶ [core-api] I≈0 A≈1.0 interfaces + value types [consumer-c] ─┘ ▲ │ implements [core-impl] I≈1 A≈0 concrete, nobody depends on it ``` What changed and why the metrics move: - **`core-api`** contains only interfaces (and immutable value/DTO types). It has no outgoing dependencies (`Ce = 0`) and all the incoming ones ⇒ `I ≈ 0`; `Na/Nc ≈ 1` ⇒ `A ≈ 1`. Point: **(0,1)** — the ideal stable-abstract corner, `D = 0`. - **`core-impl`** holds the concrete classes. Consumers no longer name it (they get instances via a factory, DI container, or composition root), so `Ca → 0` ⇒ `I ≈ 1`; it is all concrete ⇒ `A ≈ 0`. Point: **(1,0)** — the ideal concrete-unstable corner, `D = 0`. - The **source dependency is inverted**: `core-impl` now points at `core-api`, opposite to the flow of control. That's Dependency Inversion applied to components, and it is exactly what the Stable Abstractions Principle asks for. - Practical consequence: the volatile implementation can now change **without recompiling or redeploying a single consumer**, as long as the contract holds. Wiring: something has to bind interface to implementation. That's the **composition root / main component** — itself an unstable, concrete leaf where it belongs. A DI container, a factory in `core-api` returning an implementation via a service loader, or explicit construction in `main` are all fine. ### Step 4 — Alternatives worth considering - **Don't share it at all.** If two teams each need 60% of a utility, duplicating a small helper is frequently cheaper than a shared, coordinated component. (Shared code is coupling; the Reuse/Release Equivalence trade-off.) - **Freeze it deliberately.** If the churn is accidental (people dumping unrelated helpers in), the fix may be a policy plus a smaller API rather than a refactor. - **Version it.** For a genuinely shared concrete contract you cannot invert (e.g. wire formats), semantic versioning plus additive-only evolution decouples release timing even without abstraction. ### Step 5 — Lock it in - **Re-measure** A, I, D per component; expect the new pair to land near the line. - **Add an architecture test** (ArchUnit / dependency-cruiser / Modulith verification / NDepend rule) asserting that consumers depend on `core-api` and never on `core-impl`. Without this, the first hurried PR re-couples them and the metrics silently regress. - **Track D as a trend** in CI rather than a hard gate, so you see regressions without incentivising interface-for-interface's-sake. ### Anti-patterns while doing this - **Interfaces in the same component.** Adding `interface Foo` next to `class FooImpl` inside `shared-core` raises A but leaves `Ca` untouched — consumers still recompile on every change. The interface must live in a component the consumers depend on and the implementation does not. - **One interface per class, mechanically.** Extract contracts at real variation points, not by reflex; a 1:1 mirror package is ceremony with a maintenance cost. - **Leaking concrete types through the contract.** If `core-api` interfaces return `core-impl` types, the source dependency isn't inverted and `Ca` on the impl stays high. - **Refactoring by metric alone.** Verify the churn is real; a low-D-scoring but frozen module is not worth the risk.

  • Why doesn't adding interfaces inside the existing module fix the problem?
    Because Abstractness rises but Instability doesn't move: consumers still import the module that also contains the volatile implementations, so afferent coupling stays high and every change still forces their rebuild/retest/redeploy. The point moves up the A axis but the *coupling* is unchanged. The inversion only pays off when the interfaces sit in a component the consumers depend on and the implementations do not.
  • After the split, what stops someone from re-coupling consumers directly to the implementation component?
    An enforced architecture rule: an ArchUnit/dependency-cruiser/Modulith verification test, a build-tool visibility restriction (Java modules, internal/friend visibility, package-private factories, a `api`/`implementation` Gradle configuration split), or a CI check on the dependency graph. Metrics detect the regression; only an enforced rule prevents it.
  • When would you choose duplication over extracting a shared abstraction here?
    When the "shared" code serves consumers with genuinely different reasons to change, and sharing forces release coordination between independent teams. A small duplicated helper that each team evolves freely is often cheaper than a shared component plus its contract, versioning, and cross-team negotiation. The Common Reuse Principle and Reuse/Release Equivalence both point this way.

A shared kitchen everyone cooks in. Right now every cook walks into the same room and touches the same appliances, so any remodel shuts down every meal. You replace it with a serving hatch (the contract): cooks order through the hatch and never see the kitchen, so you can gut and rebuild the kitchen mid-service without closing the restaurant.

saying these in an interview costs you the question

  • Adding interfaces beside their implementations in the same component and declaring the problem solved
  • Refactoring purely on the metric without checking whether the component actually changes
  • Skipping the CCP/CRP split and abstracting a grab-bag wholesale, producing a giant mirror interface package
  • Letting contract interfaces return or accept concrete implementation types, which leaves the dependency un-inverted
  • Finishing without an enforced architecture rule, so consumers silently re-couple to the implementation component

context