How would you measure coupling in a codebase, and when is deliberately accepting higher coupling the better engineering decision?
answer
- Ce, Ca, I = Ce/(Ca+Ce), abstractness, main sequence
- Zone of pain vs zone of uselessness
- Cycles = worst structural coupling
- Change coupling from git history beats static graphs
- Volatility, not count, prices a dependency
basics
~20 sCount incoming dependencies (afferent) and outgoing ones (efferent) per module, plus dependency-cycle checks. Accept higher coupling when the alternative is layers of indirection nobody needs — for stable, rarely-changing dependencies, or in small, short-lived, or performance-critical code.
solid answer
~50 sMeasure with dependency metrics: **efferent coupling (Ce)** — how many things a module depends on; **afferent coupling (Ca)** — how many depend on it; **instability I = Ce/(Ca+Ce)** (0 = maximally stable, 1 = maximally unstable); **abstractness A** and the distance from the main sequence |A+I-1|, plus cycle detection, fan-in/fan-out, and change-coupling mined from version-control history (files that always change together — a coupling that static analysis cannot see). Enforce in CI with architecture-fitness tests (ArchUnit, dependency-cruiser, Modulith verification). But the metric is not the goal. Accept coupling when the dependency is **stable** (standard library, mature value types), when removing it costs an indirection layer with no realised option value (speculative generality), when performance or transactional consistency demands directness, or when the module is small and short-lived. Also remember low static coupling can hide high semantic or temporal coupling — a distributed monolith has beautiful metrics and terrible change isolation.
code
pseudocode · 7 lines// architecture fitness function: turn the principle into a CI failure
rule("domain must not depend on infrastructure") {
noClasses().that().resideIn("..domain..")
.should().dependOnClassesThat().resideIn("..infrastructure..")
}
rule("no cycles between modules") { slices().matching("..app.(*)..").should().beFreeOfCycles() }go deeper
Say that you can count how many classes a class imports/uses and how many use it, and that fewer outgoing dependencies is usually simpler.
Name afferent/efferent coupling, fan-in/fan-out, and cycle detection, and mention enforcing rules in CI so the design does not drift.
Add instability, abstractness, the main sequence and its two failure zones, plus change coupling from commit history, and give concrete cases where accepting coupling is correct (stable dependencies, performance, transactional consistency).
Frame it economically — decoupling buys an option, indirection is its premium — cover fitness functions as governance, the distributed-monolith failure mode, and how organisational boundaries (Conway's law) determine which couplings actually cost the business.
## Part 1 — How to measure coupling ### Static structural metrics (Robert Martin's package metrics) - **Efferent coupling `Ce`** (outgoing/fan-out): number of distinct external elements this module depends on. High Ce = fragile; many things can break it. - **Afferent coupling `Ca`** (incoming/fan-in): number of external elements depending on this module. High Ca = rigid; changing it is expensive because many depend on it. High Ca is *not* bad per se — it is what a well-used utility looks like — but it obliges stability. - **Instability `I = Ce / (Ca + Ce)`**, in [0,1]. `I = 0` means nothing depends outward and many depend inward → maximally stable, hard to change. `I = 1` means it depends on everything and nothing depends on it → free to change. - **Abstractness `A`** = abstract types / total types, in [0,1]. - **Main sequence** — the healthy line `A + I = 1`. **Distance `D = |A + I − 1|`**. Two failure zones: the *zone of pain* (concrete and stable — heavily depended upon yet impossible to change), and the *zone of uselessness* (abstract and unstable — abstractions nobody uses). - **The Stable Dependencies Principle**: depend in the direction of stability. **The Stable Abstractions Principle**: a stable module should be abstract. Both are the architectural restatement of Low Coupling. ### Structural checks beyond counts - **Cycle detection.** Dependency cycles between modules are the highest-severity coupling defect: nothing in a cycle can be understood, tested, built, or deployed independently. The Acyclic Dependencies Principle says cut them (usually by dependency inversion or by extracting a shared abstraction). - **Depth of reach / Law of Demeter violations.** `a.b().c().d()` couples one caller to three types. - **Interface surface size.** A module exposing 200 public symbols is more coupled than the edge count suggests, because consumers can bind to anything. ### Behavioural / historical metrics — the ones people forget - **Change coupling (logical coupling)**, mined from version control: which files or modules keep appearing in the same commits? This exposes coupling with *no static edge at all* — duplicated business rules, an implicit protocol, a shared assumption. It is often the most predictive signal you can get, and requires only commit history. - **Co-deployment / co-release frequency** at the service level: if two services must always ship together, they are one module wearing two costumes. - **Test-mock density**: a unit test needing eight mocks reports its subject's coupling honestly. ### Enforcement Metrics that are only reported get ignored. Convert them into **architecture fitness functions** in CI: ArchUnit (JVM), dependency-cruiser (JS/TS), Spring Modulith verification, import-linter (Python), NetArchTest (.NET). Rules like "domain must not depend on infrastructure", "no cycles between modules", "module X may depend only on Y and Z" turn a design principle into a build failure. ## Part 2 — When higher coupling is the right call Low Coupling is a **means**, and every unit of decoupling is bought with indirection, more elements, and more cognitive hops. Accept the coupling when the price exceeds the benefit: 1. **The dependency is stable.** Depending on the language's standard collections, a mature date library, or a frozen value type generates almost no rework. Wrapping stable things in your own abstraction layers is pure overhead. Volatility, not existence, is what makes a dependency expensive. 2. **No realised option value — speculative generality.** Interfaces with exactly one implementation, created "in case we swap the database", are a cost with an unexercised option. Prefer the direct dependency until a second implementation or a genuine change-driver appears; the refactoring is usually cheap when it *does* appear. 3. **Performance and transactional consistency.** Indirection layers, event hops, and network boundaries cost latency and turn atomic in-process updates into distributed-consistency problems. A tightly-coupled in-process call that stays inside one transaction is frequently the correct engineering trade. 4. **Small, short-lived, or single-owner code.** A migration script, a spike, a one-quarter internal tool: change-isolation value is near zero, so pay nothing for it. 5. **Cohesive units that belong together.** Coupling *inside* a well-bounded module is cheap and expected; only coupling *across* boundaries needs a budget. Splitting a highly cohesive module to lower an intra-module metric strictly harms the design. 6. **Deliberate coupling to a stable published contract** — for example, everything depending on a shared, versioned, immutable schema of core value types. That is the Stable Dependencies Principle working as designed. ## Part 3 — The traps - **Goodhart's law on metrics.** Teams optimising `Ce` mechanically produce interface-per-class boilerplate that lowers the number while raising cognitive load. The metric is a *conversation starter*, never a target. - **Low static coupling, high semantic coupling.** A microservice fleet where 11 services must be released in lockstep for one feature is the *distributed monolith*: static coupling metrics look excellent (each service imports nothing from another), but change coupling is catastrophic, and network boundaries have made every dependency slower and less reliable. Change-coupling analysis catches it; static analysis does not. - **Temporal coupling gets worse, not better, when you distribute.** A synchronous call chain across services requires all of them to be up simultaneously; availability multiplies downward. Decoupling at the class level and coupling at the runtime level is a common net loss. - **Decoupling can move complexity rather than remove it.** Replacing a direct call with an event makes the compile-time graph clean and the runtime behaviour harder to trace. That is a real trade, not a free win.
- What is change coupling, and why can it contradict your static dependency graph?Change coupling is the empirical tendency of two files or modules to be modified in the same commits, mined from version-control history. It captures duplicated rules, implicit protocols, and shared assumptions that create no import edge at all. A codebase can have a clean static graph and still require synchronised edits everywhere — which is what actually costs money.
- A module has high afferent coupling. Is that a problem?Not by itself — many dependants is what a genuinely useful shared module looks like. The obligation it creates is *stability*: by the Stable Dependencies Principle a heavily-depended-upon module should change rarely and, by the Stable Abstractions Principle, should be largely abstract. The dangerous combination is high Ca plus high concreteness plus frequent change — Martin's zone of pain.
- Your team split a monolith into services and every feature now requires coordinated releases of six of them. What happened and how do you detect it early?That is a distributed monolith: the split followed technical or organisational lines rather than genuine change boundaries, so semantic and change coupling survived while network cost and temporal coupling were added. Detect it by tracking co-release/co-change frequency across services from day one, not by static dependency analysis, which will look fine.
Coupling metrics are like a city's road map: they show which districts connect. But traffic data — who actually drives from A to B every morning — is change coupling from your commit history. A city can look well separated on the map while everyone commutes across it daily. Plan with both.
saying these in an interview costs you the question
- Treating a coupling metric as a target to minimise rather than a signal to investigate (Goodhart's law).
- Assuming that because modules do not import each other they are decoupled — semantic, temporal, and change coupling are invisible to static analysis.
- Adding an interface for every class "to reduce coupling" when there is exactly one implementation and no evidence of variation.
- Believing a network boundary reduces coupling; it usually preserves logical coupling while adding latency, partial failure, and temporal coupling.
- Not distinguishing afferent from efferent coupling — high fan-in on a stable abstraction is healthy, high fan-out on a volatile module is not.
- Ignoring dependency cycles, which are the single most damaging structural coupling defect.