skip to content

You compute abstractness (A), instability (I), and distance from the Main Sequence (D) for every module in a large system and find several modules deep in the Zone of Pain. How would you decide what — if anything — to act on, and would you enforce these metrics in CI?

level: principalimportance: nice to knowfreq 18%

answer

  1. Metrics screen candidates; churn decides
  2. Zone of Pain + low churn = harmless
  3. Emergency: high Ca, A≈0, high churn, multi-team
  4. Gate direction (ArchUnit/Modulith), not D
  5. Track D as a trend, not a threshold

basics

~20 s

Don't act on the metric alone. Cross-reference with how often each module actually changes; a stable concrete module that never changes is harmless. Fix the ones that are widely depended on, concrete, and churning. Enforce dependency direction in CI, not metric thresholds.

solid answer

~50 s

Treat A/I/D as a **screening instrument**, not a verdict. The metrics omit the one variable that makes the Zone of Pain painful: **volatility**. A stable, concrete, never-changing module (a math library, a frozen codec, a value-type library) is harmless there — Martin says so explicitly. So overlay version-control churn, incident history, and ownership: the emergency signature is *high Ca + low A + high commit rate + cross-team edits*. For those, extract stable interfaces into a contracts component and push volatile implementation into unstable adapters. In CI, avoid gating on D: it's gameable with empty interfaces, blind to dependency weight and runtime coupling (shared databases, message schemas, HTTP contracts), and legitimately high for composition roots and UI adapters. Instead encode intended **direction and boundaries** with structural tests — ArchUnit, Spring Modulith verification, dependency-cruiser, module declarations — and track D as a per-release *trend* on a dashboard to catch drift.

go deeper

for a junior

Say the metric alone isn't a verdict: check whether the module actually changes often before refactoring it.

for a middle

Add the churn overlay and the concrete refactor (split api from impl, move the port to the consumer), and note that composition roots are legitimately off the line.

for a senior

Argue against metric gates with specific failure modes (gameability, false confidence, suppression sprawl) and propose structural enforcement — ArchUnit/Modulith/dependency-cruiser, acyclicity, api-only imports — plus D as a trend.

for a principal

Frame it as risk and coordination economics across the deployment graph: identify load-bearing components, price the change cost, sequence incremental extraction, align ownership and release cadence with the stable/unstable split, name what the metrics cannot see (runtime and data coupling), and measure the outcome as lead time and rebuild blast radius rather than as a metric target.

### What the numbers do and don't say Recap of the metrics, per component (jar/module/package): - `A = Na/Nc` — abstract types over total types. - `I = Ce/(Ca+Ce)` — outgoing couplings over total couplings; `I=0` maximally stable (many dependents), `I=1` maximally unstable. - `D = |A + I − 1|` — distance from the Main Sequence `A + I = 1`. Zone of Pain = low A, low I. Zone of Uselessness = high A, high I. They are cheap, computable from static analysis, and genuinely good at *surfacing candidates*. They are poor at *deciding*, for six reasons: 1. **Volatility is invisible.** SAP's entire economic argument is about the cost of *change*. A stable concrete component that never changes costs nothing. Martin himself lists `String`-like and math-library components as legitimate Zone-of-Pain residents. 2. **Gameable.** Add marker interfaces, split a class into an interface plus one implementation, and A rises with no design improvement. Any metric used as a gate eventually gets optimized rather than satisfied. 3. **Coupling weight is flattened.** Ca and Ce count arrows. One call to a formatting helper counts the same as deep structural dependence on twelve internal types. 4. **Runtime coupling escapes entirely.** Two modules with zero import edges can be lock-step coupled through a shared database schema, a Kafka message contract, an HTTP API, reflection, or dependency-injection wiring. Static analysis reports them as independent. 5. **Abstraction quality is invisible.** A 40-method god interface and a set of cohesive, Interface-Segregation-compliant interfaces can produce identical A. 6. **Some components should be off the line.** The composition root / `main` is correctly the most unstable and most concrete thing in the system. UI adapters, generated clients, migration modules, and test fixtures likewise. ### A decision procedure **Step 1 — enrich the data.** Join per-component A/I/D with: - **Churn**: commits, changed lines, and number of distinct authors per component over the last 6–12 months (from `git log`). - **Blast radius**: transitive dependent count, not just direct Ca. - **Incident/defect history** attributed to the component. - **Ownership**: how many teams edit it. **Step 2 — triage.** | Signature | Reading | Action | |---|---|---| | Low A, low I, **low churn** | Benign Zone of Pain (stable utility, value types, frozen codec) | Leave it. Document the intent. | | Low A, low I, **high churn**, multi-team | **Real rigidity** — the expensive case | Extract contracts; invert the volatile part | | High A, high I | Zone of Uselessness | Delete unused abstractions; collapse single-implementation ports | | Off-line composition root / adapters | Intended | Exclude from the analysis explicitly | **Step 3 — refactor the real cases.** Standard moves: - **Split api from impl**: extract the interfaces the dependents actually use into a small, stable `*-api` component; move implementations into a new unstable component that depends on the api. Dependents now depend on the abstraction; A rises where I is low, and the volatile code lives where change is cheap. - **Move the port to the consumer.** If a high-level component needs a capability, the interface belongs to the high-level component; the provider implements it. This flips the arrow toward stability (SDP) as well as toward abstraction (SAP). - **Break god-utility modules.** A `common`/`shared-utils` jar is a Zone-of-Pain magnet because it aggregates unrelated concerns with maximal Ca; split it by cohesion (Common Closure / Common Reuse principles) rather than adding interfaces to it. - **Attack shared-database coupling directly** — no amount of interface extraction fixes it, because the coupling is in the schema, not the imports. - **Sequence the work**: extract the api component first, migrate dependents incrementally (the old concrete types can temporarily implement the new interfaces), then move the implementation. **Step 4 — measure the outcome that matters**, not the metric: did lead time for changes in that area drop? Did cross-team coordination on releases fall? Did the number of components rebuilt per change shrink? ### CI: what to enforce and what not to **Don't gate on D or A.** Thresholds produce two bad behaviours: teams add empty interfaces to pass, and legitimate outliers (composition roots, generated code) need permanent suppressions that erode the signal. Metric gates also give false confidence — a system can be fully within thresholds and still be coupled through a shared database. **Do gate on structure**, which is unambiguous and non-gameable: - **Dependency direction rules**: ArchUnit `layeredArchitecture` / `noClasses().should().dependOnClassesThat()`, Spring Modulith `ApplicationModules.verify()` with `allowedDependencies`, dependency-cruiser or ESLint `import/no-restricted-paths` for TypeScript, Java Platform Module System `requires`/`exports`, .NET architecture tests. - **Acyclicity**: no dependency cycles between components (Acyclic Dependencies Principle) — a hard, objective rule. - **"Impl components are not importable"**: consumers may only import `*-api`. **Do publish A/I/D as a trend**, per release, on a dashboard or in a review ritual. Drift away from the Main Sequence over time is a far better signal than any absolute threshold, and it invites a conversation rather than a bypass. ### Organizational angle Stable components are, by definition, the ones many teams depend on. That makes them a **coordination cost centre**, not just a code-quality question: their release cadence, versioning policy (semantic versioning, deprecation windows), and ownership matter more than their A value. Conway's Law bites here — if a stable abstract component has no clear owner, it accretes concrete convenience code from every team and slides into the Zone of Pain regardless of what CI says.

  • A team proposes a CI rule failing the build when any module's D exceeds 0.5. What's your response?
    Push back. D is trivially gamed by adding empty interfaces, legitimately high for composition roots, generated clients, and UI adapters, blind to volatility and dependency weight, and completely blind to coupling through shared databases or message contracts. I'd instead gate on objective structural facts — no dependency cycles, no importing implementation components, no inward-to-outward layer edges — and report D as a per-release trend for review.
  • How do you tell a harmless Zone-of-Pain module from a harmful one without running the refactor first?
    Cross-reference churn and blast radius. Pull commit counts, changed lines, and distinct author/team counts for the module over the last 6–12 months, plus its transitive dependent count and defect history. Frozen, single-owner, rarely touched modules with many dependents are fine. Modules with many dependents that multiple teams edit every sprint are the expensive ones — the cost is coordination, not the metric.
  • What coupling does this whole analysis miss entirely?
    Everything not expressed as a static import edge: a shared database schema, message/event payload contracts, HTTP API shapes, reflection and dependency-injection wiring, feature-flag semantics, and temporal coupling in deployment order. Two modules can score perfectly on A/I/D and still be impossible to release independently.

saying these in an interview costs you the question

  • Refactoring every Zone-of-Pain module without checking whether it actually changes
  • Adding empty or single-implementation interfaces purely to raise A and pass a metric gate
  • Setting a hard D threshold in CI and treating it as an architecture guarantee
  • Assuming a low D means components can be released independently — shared databases and message contracts say otherwise
  • Comparing D values across tools without noting that some normalize by √2 (max ≈ 0.707)
  • Ignoring the composition root's legitimate position at the concrete/unstable extreme

context