Striff Engineering Architecture
Coupling metrics, explained — and why a coupling delta is not a finding
What afferent and efferent coupling actually measure, how to read them together with complexity, inheritance depth and encapsulation, and the case for why none of these numbers should ever be reported to a reviewer as a finding on its own.
On this page
Every architecture tool eventually shows you two numbers: afferent coupling and efferent coupling. Most engineers nod, half-recall that the definitions point in opposite directions, and move on. That is a shame, because read together they are the closest thing software has to a blood-pressure reading for a component.
They are also a trap. It is tempting to report changes in those numbers as findings on a pull request: “efferent coupling grew by 4.” This post is both halves of that: what the numbers mean and how to read them, and why a delta on one of them should not be allowed to interrupt a reviewer.
The two directions
Both metrics count dependencies on a single component — a class, or a package. The only difference is which way the arrows point.
Afferent coupling (Ca): arrows in. How many components depend on you. This is your blast radius: if you change, this is how many places can break.
Efferent coupling (Ce): arrows out. How many components you depend on. This is your exposure: every outgoing arrow is a reason you might be forced to change.
Same node, opposite questions
Illustrative components. The shapes are the point: a repository that half the system calls, and an orchestrator that calls half the system, are opposite risks that the same word — "coupling" — covers.
The definitions come from Robert C. Martin’s package-design metrics, which also give the derived number worth knowing: instability, I = Ce / (Ce + Ca). High Ca and low Ce is stable: hard to justify changing, safe to depend on. High Ce and low Ca is unstable: free to change, dangerous to depend on. Neither is bad by itself. Problems start when a component is high on both axes at once.
The four quadrants
Reading Ca and Ce together
Stable core
Interfaces, domain types, shared contracts. Everyone depends on them; they depend on little. Healthy, but every change here is expensive by design.
The danger zone
Many dependents and many dependencies: god classes, "utils" dumping grounds, accidental bridges. Fragile to change, impossible to avoid.
Quiet leaf
Self-contained helpers and features. Change freely; almost nothing can break.
Orchestrator
Controllers, entry points, wiring code. Volatile but safe: nothing depends on them, so their churn does not ripple.
The quadrant a component sits in matters more than either raw number. A Ce of 40 on an orchestrator is Tuesday. A Ce of 40 on a stable core component means every one of its many dependents inherits forty new reasons to break.
The rest of the panel
Coupling is two gauges. On a diagram of a change, each component carries a few more, each with its delta:
The numbers on a component, decoded
| Badge | What it measures | What it hints at |
|---|---|---|
| WMC | Weighted method complexity: the summed cyclomatic complexity of the class's methods | The best single "is this becoming a god class?" gauge. Logic pooling here instead of being distributed. |
| DIT | Depth of inheritance tree: how many ancestors the class has | Deep hierarchies make behaviour hard to trace; every layer is somewhere logic can hide. |
| NOC | Number of children: direct subclasses | A contract many things extend. Like high Ca, it multiplies the cost of every change made here. |
| ENC | Encapsulation ratio: the share of members that are private or protected | Falling ENC means internals are being exposed, inviting exactly the coupling the other numbers then measure. |
| AC / EC | Afferent and efferent coupling, as above | Blast radius and exposure. |
Read together the panel tells a story no single number can: rising WMC with rising EC and falling ENC is a class absorbing responsibilities, reaching for more collaborators, and opening its internals to do it. That is a god class three pull requests before anyone in review would name it one.
Now the uncomfortable part
Everything above is true, and none of it should be a finding on a pull request.
It is easy to see why tools report them anyway. “Efferent coupling grew from 8 to 17.” “Weighted method complexity grew by 10.” They are easy to compute, they look like analysis, and there is always one to report. The argument against them has four parts.
Why a metric delta is not a finding
No action follows
"Coupling grew by 4" — and then what? Every action a reviewer might take next requires knowing which four and toward what. The number by itself terminates in a shrug.
A linter does it better
Thresholds on complexity and fan-out are a solved problem, per file, in your existing pipeline, with configuration you control. Reimplementing that in a review comment is worse at the same job.
The number is already on the page
Every one of these values, with its delta, is printed on the component itself. A finding that restates a label six inches away is not information, it is repetition.
It crowds out the rest
Two thirds of the output being restatement means the one row that needed a human is two thirds less likely to be read. Volume is not free; it is paid for out of the same attention budget.
None of this is an argument against the metrics. They belong on the component, where a reader can look them up, and in deciding what to read first. What they should not have is the right to interrupt.
A better test for any candidate finding is a single question: does a reviewer already know this from the diff or from the diagram? A metric delta fails it. So does an added import, which is a line of the diff. What passes is what needs more than the diff to know at all: a sentence in your own documentation that this change made false, or a dependency your team has written down must not exist.
When fan-in does earn a finding
There is one place a coupling number legitimately reaches the reviewer, and the difference is instructive.
In Activiti/activiti-cloud #2552, the public interface IntegrationResult loses the method getIntegrationRequest(). At least twelve components reference that type.
That is the whole distinction. A metric is a property of the code. A finding is an event — something this change did — with a property of the code attached to say how much it matters. Reporting the property without the event is how a tool ends up with a lot to say and nothing worth reading.
How to actually use these numbers
- Read the quadrant, not the value. A Ce of 40 means nothing until you know the Ca. Instability, not either raw number, is the thing that tells you whether a component is safe to depend on.
- Watch high-Ca components the way you watch production config. Any change touching a component with dozens of dependents deserves a closer read, especially when the diff looks trivial. Small diffs on high fan-in nodes are where blast-radius accidents live.
- Use metrics to decide reading order. That is what they are good for: not “look at this”, but “look at this first”.
- Do not set thresholds and argue about them. “Ce must stay under 20” produces meetings, not architecture. If you want a hard gate on complexity, put it in your linter where it belongs, and let structural review answer the questions a linter cannot see.
The questions a linter cannot see are the ones worth writing down: which module may depend on which, and what lives where. Once they are sentences in your architecture docs, Striff checks each one at both revisions of every pull request and quotes the one a change broke. Install the GitHub App, and keep the metrics on the diagram, where you can read them, and out of your notifications, where they cannot help.