CouplingSIMULATEDCONTESTEDLANGUAGE-SPECIFIC

Afferent and Efferent Coupling

Who depends on me, and what do I depend on. A useful pair of questions, a widely published pair of metrics, and a gap between the two that is worth being honest about.

The requirement, the obvious build, and why it breaks

Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.

The question

Given who depends on this module and what it depends on, what can I actually conclude — and what am I about to over-conclude?

The requirement

An architecture review wants a "coupling health" page in the internal dashboard, with a per-module figure and a threshold that turns red. The intent is genuinely good: nobody wants to find the structural problems during the next migration.

The obvious build

Compute afferent coupling — the count of modules that depend on this one — and efferent coupling, the count it depends on. Combine them into an instability figure, chart it per module, set a threshold, and let the trend tell us where the rot is.

Why it breaks

The two counts answer genuinely different questions and the combination discards that. Many dependents is an obligation to be stable; many dependencies is exposure to other people's churn. Averaging them into one figure produces something with no reading at all (Fan-in and Fan-out).

How it breaks as requirements change
  • The two counts answer genuinely different questions and the combination discards that. Many dependents is an obligation to be stable; many dependencies is exposure to other people's churn. Averaging them into one figure produces something with no reading at all (Fan-in and Fan-out).
  • The graph is incomplete in exactly the places that matter. The eleven-step pipeline coupled through a mutable context has almost no import edges and is the most expensive module in the system.
  • It is trivially gameable, and it will be gamed without anyone intending to: a facade collapses many edges into one, a re-export module hides a dozen, and dynamic wiring erases them entirely (Facade).
  • Once it is on a dashboard with a threshold it stops being a signal. Modules get split to move a number, which adds edges elsewhere and improves nothing (Over-Decomposition).
  • Worst, it displaces the question it was meant to prompt. "Is this module red" is easy to answer and worthless; "what changes here, how often, and who pays" is the actual finding and no graph contains it (Change Amplification).
RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

What limits the solution, and what must never stop being true

This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.

Constraints
  • The only data available cheaply is the static import graph, which sees nothing that couples through shared state, events, configuration or a shared database table (Shared-State Coupling).
  • Whatever is published will be used in performance conversations, whether or not that was intended.
  • The codebase mixes a modular core with two legacy areas that would dominate any ranking, and everyone already knows about those.
Invariants
  • Nothing presented as a measurement may be a model output. If a number comes from a heuristic over an incomplete graph, it is labelled as one wherever it appears.
  • No structural change is made because a number moved. The number opens an investigation; the investigation decides.

Who owns what, and where the seams fall

Responsibilities decide boundaries; boundaries decide what an interface has to say.

Responsibilities
  • A module with many dependents owns stability: it took on a compatibility obligation the moment it was widely adopted, whether or not anyone said so (API Stability).
  • A module with many dependencies owns the consequences of its dependencies' churn, and owns choosing stable ones where it has a choice (Volatile Dependencies).
  • Whoever publishes a metric owns its reading. A number on a dashboard with no interpretation attached will acquire one, and it will be the wrong one.
Boundaries
  • The useful boundary this pair suggests: things with many dependents should depend on few things, so that instability does not propagate through them (Stable Dependencies).
  • That rule has a real reading — depend in the direction of stability — and it is worth more than any figure derived from it.
  • The boundary the metric cannot see is the one that matters most: two modules writing the same database rows have no import edge and are as coupled as code gets (State Ownership).

Two questions, and they are not the same question

Afferent means incoming: how many modules depend on this one. Efferent means outgoing: how many it depends on. Both are worth asking and they have opposite implications, which is why compressing them into a single figure destroys most of the value.

The classical formulation goes one step further and defines instability as the share of a module's edges that point outward — all outgoing means maximally unstable, all incoming means maximally stable — with the rule that a module should depend only on modules at least as stable as itself. That rule has a genuine reading and it is the part worth keeping: depend in the direction of things that change less often than you do (Stable Dependencies). The figure itself adds very little to that sentence.

  • The useful sentence in this whole area is one line long: things that many modules depend on should themselves depend on very little.
  • Everything below that sentence — the ratio, the abstractness pairing, the distance-from-ideal figure — adds precision that the underlying data does not support.
  • Each row's "missing variable" is available from your commit history, which is a real measurement of your codebase rather than a model of one (Finding Seams).
Afferent — who depends on meEfferent — what I depend on
What it constrainsMy freedom to change. Every dependent is someone who did not consent to my churn.My stability. I cannot be more stable than the least stable thing I import.
The obligation it createsCompatibility. Once widely adopted, the interface is a contract whether or not it was declared as one (API Stability).Choice. Where a dependency is optional, prefer the one that changes less often (Volatile Dependencies).
When it is healthyA dependency-free value type or domain rule that everything uses and nothing has to fear (Stable Boundaries).A composition root, controller or orchestrator whose entire job is to wire things together (Wiring and the Composition Root).
When it is a findingIt is also changing frequently — then it is a change amplifier and every edit ripples (Change Amplification).It also holds rules — then it has acquired every reason to change that its dependencies have (God Object).
The variable it is missingHow often the module changes. Without it the count means nothing at all.How volatile each dependency is. Twelve stable ones cost less than three volatile ones.
What it cannot seeCoupling through shared state, events, config order or a shared table — none of which produce an import edge (Shared-State Coupling).The same, plus anything wired dynamically at runtime (Service Locator).

The same two counts, two opposite readings

SIMULATEDThese rows are generated by an Engineer Atlas model of a mid-size codebase to make the contrast legible; they are not measurements of any real repository and the counts are illustrative. What transfers is the relationship between the columns — specifically that rows one and two are indistinguishable without the third, and that row six is invisible in the first two regardless of what the third says.

Here is the report the dashboard would produce, with the column it cannot produce added by hand. Read the first two columns alone and the two modules are indistinguishable. Add the third and they are opposites: one is the codebase's best asset and the other is where a third of the churn originates.

That third column is not exotic. It is a git log over six months, grouped by path. The reason it is not on the dashboard is that nobody thought to put it there — which is a much better argument for improving the report than for trusting it.

module                     dependents   dependencies   changes/6mo   reading
──────────────────────────────────────────────────────────────────────────────
domain/money                      212              0             1     asset: stable, depended on, depends on nothing
platform/formatting               198              7            34     the finding: broadcasts everything it inherits
checkout/orchestrator               2             19             9     fine: composition is its job, holds no rules
pricing/rules                      31              1             6     fine: high-value core, worth heavy tests
platform/common                   241             23            41     the bucket. Split by reason to change
orders/pipeline-steps               4              1            12     invisible here: 11 steps coupled by a shared
                                                                        mutable context, one import edge between them

  columns 1-2: static import graph.  column 3: git log.  column 4: a person.
  the tool produces two of these four. the two it produces are the two that
  cannot be read on their own.

The dashboard as a smell

Coupling dashboards are built by people trying to do the right thing, which is exactly why this is worth naming as a smell rather than as a mistake. The instinct — monitor structure the way we monitor latency — is sound. The problem is that the available data does not support a threshold, and a threshold is what gets built.

The version that works keeps everything except the threshold: same counts, same graph, attached to a module with an owner, read as a trend, and always accompanied by the question the numbers cannot answer.

smellA structural metric with a threshold

looks like A page of per-module coupling figures with red and green cells, or a build step that fails when a derived instability or complexity figure crosses a line. Often accompanied by a quarterly goal to reduce it.

suggests That a real concern about structural decay has been converted into the most measurable proxy available, and that the proxy will now be optimised. Expect facades, re-export modules and splits that move edges around, and expect the genuinely expensive coupling to stay invisible because it produces no edges (Facade).

fix Keep the graph, drop the score. Publish dependents, dependencies and change frequency as three separate columns; add a fourth column that a person writes. Replace the threshold with a review that looks at the top few rows by hand and asks what changes there, how often, and who pays (Design Review).

when this is fine Two graph-derived rules do belong in a build, because they have a single unambiguous reading and cannot be satisfied by indirection: no new cycles, and no imports across a declared boundary. Both encode a decision a person made rather than a number a tool computed. A per-module trend with a named owner, reviewed by that owner and used to start investigations rather than to close them, is also genuinely useful — that is the practice the opposing camp is defending, and it is not what most dashboards are (Internal Module Contracts).

How to build it

Most important first.

  • Keep the two counts separate and read each with a second variable. Dependents matter only alongside change frequency; dependencies matter only alongside the volatility of what is depended on (The Cost of Change).
  • State the stability rule in its useful form: a module should not depend on something less stable than itself, where stability means "changes rarely" and not "is well written" (Dependency Direction).
  • Use the graph to generate a shortlist, then look at the modules by hand. That is a slow process and there is no fast version that works (Design Review).
  • Correlate with commit history before concluding anything. Change frequency per module is the variable the import graph is missing, and it is cheap to compute from the same repository (Finding Seams).
  • Publish trends for a specific module with a named owner, not thresholds across a codebase — a rising figure on one module is a question worth asking, a red cell in a table of two hundred is not (Revisit Triggers).
  • If you build the dashboard anyway, label every figure as derived from static imports and list what it cannot see, on the page itself.

What the next change costs

The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.

Cost of the next change
  • A module with many dependents: the next change to its interface costs every dependent, so the useful design response is to make the interface change rarely rather than to reduce the count (Stable Boundaries).
  • A module with many dependencies: the next change to any of them can cost this module, so its expected cost is the sum of its dependencies' change rates — which is a genuinely useful way to think and requires no dashboard.
  • A module with both: it is a transmitter. Everything it inherits, it broadcasts, and that combination is the one finding this pair of numbers reliably produces (The Common Module).
  • The cost of the dashboard itself: a page to maintain, a number people optimise, and a false sense that the structural question is handled — which is paid every quarter, quietly, by the investigations nobody runs because the page is green.
What the recommended approach costs
  • Refusing to publish numbers means the structural conversation depends on senior attention, which is scarce and unevenly distributed — that is a genuine cost, and it is the strongest argument for the dashboard.
  • The interpretive version of this — read counts with change frequency, look at modules by hand — does not scale past a codebase one person can hold in their head.
  • Being honest that the figures are weak evidence reduces their persuasive power in exactly the rooms where structural work needs to be argued for, and that is a real loss, not a rhetorical one.

What can go wrong

Failure modes
  • The figure becomes a target and modules are restructured to move it, which is Goodhart's law arriving on schedule and with good intentions (What to Automate Out of Review).
  • A team is judged on a number produced by a heuristic over an incomplete graph, and stops reporting real structural concerns because the dashboard says the area is green (Tone, Disagreement and Receiving Review).
  • The genuinely expensive coupling — shared mutable state, event ordering, a shared table — stays invisible, and its invisibility is now endorsed by a page that claims to measure coupling.
  • The mitigation fails on its own terms: "read it carefully with context" is a discipline that survives about two quarters, after which the page is read by people who were not in this conversation and the caveats are a footnote.
Dependencies, and their direction
  • The metric depends on the import graph, which depends on the language's module system and on how much of the wiring is dynamic — so it is not comparable between codebases, and barely comparable between two areas of one codebase.
  • Any conclusion drawn from it depends on change frequency data it does not include.
  • The organisation depends on the dashboard once it exists, which is a dependency on a model and is the one nobody declares (The Model Is a Dependency).
Misreads
  • "High instability is bad." The term names a position on a ratio of outgoing to total edges, not a defect. A leaf module that depends on several things and has no dependents is at the unstable end of that ratio and is exactly what a feature module should look like (Fan-in and Fan-out).
  • "Stable means well designed." It means it changes rarely, which is a statement about change frequency and about who depends on it. A frozen mess is stable (What "Legacy" Actually Means).
  • "The metrics say this module is a problem." The metrics say this module has many edges of a kind the tool can see. Whether that is a problem depends on what the module is for and how often it changes, and neither is in the graph.
  • "So ignore dependency analysis." No — cycles, direction rules and "what depends on what" are all genuinely useful, and the graph answers them well. It is the derived scores and the thresholds that do not survive contact with a real codebase (Dependency Cycles).
Smells this explains
  • god-object
  • utility-dumping-ground
  • shotgun-surgery

Testing it, and how it ages

What to test, and at which boundary
  • A cycle check is the one graph-derived rule worth failing a build on, because it has a single unambiguous reading (Dependency Cycles).
  • A direction rule — this package may not import that one — is the second, and it encodes a decision someone made rather than a number someone computed (Internal Module Contracts).
  • Use dependent counts to prioritise a limited testing budget: test what most things depend on, first. That is a use of the metric that does not require it to be a verdict.
  • Do not fail a build on a coupling figure. The refactors it provokes are indirection, and indirection satisfies it (Facade).
How this design ages
  • Afferent counts only grow for anything useful, so a rising line means the module got adopted. Read that as an increasing obligation to be stable, not as decay (API Stability).
  • Efferent counts grow as a module accretes responsibilities, and a steadily rising one on a module that holds rules is the most reliable signal in this lesson (God Object).
  • The metrics get less meaningful over a codebase's life as wiring becomes more dynamic — dependency injection containers, plugin registries, event buses all erase edges the analyser used to see, so the trend can improve while the structure worsens (Service Locator).

Where this applies

This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.

  • SIMULATEDEvery count in this lesson comes from an Engineer Atlas model of a mid-size codebase; none is measured from a real repository, and no threshold here is calibrated against anything. What transfers is the *shape* of the reading — a dependency-free type accumulating dependents means something different from a rule-holding module accumulating dependencies — not any figure. Run the same two counts on your own repository, where they are real, and cross them with commit frequency, which is also real.
  • CONTESTEDThe strongest opposing view, held by people who have used these metrics seriously for years: tracked as trends on individual modules with a named owner, afferent and efferent counts are among the very few structural signals obtainable without expert attention, they reliably flag emerging god objects and shared buckets before anyone notices by hand, and refusing to use them means most organisations monitor structure not at all. That case is strong and the practice it describes — trend, per module, with an owner and an investigation — is defensible. The disagreement is narrower than it looks: it is about thresholds and dashboards, which is what organisations actually build, and about whether a number computed from an incomplete graph can survive being put next to a team's name.
  • LANGUAGE-SPECIFICThe graph these counts are computed from is only as complete as the module system makes it. Go and Rust imports are explicit and static, so the graph is close to the truth; Java with reflection and dependency injection, or Python with dynamic imports, hides a large share of real edges, which makes the same metric substantially weaker evidence in those codebases than in the first two.

Where the depth lives

This domain teaches the codebase-level structure and hands the rest off.

Architecturemodular-monolith
Domains that do not exist yet
  • Testing & Reliability Engineering — dependent counts are a defensible way to prioritise a limited testing budget, which is one of the few uses of these numbers that does not require them to be a verdict.
  • System Design — the same pair at service grain is request fan-in and fan-out, where the numbers are measured rather than modelled and therefore mean considerably more.