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.

6 min read

On this page
  1. The two directions
  2. The four quadrants
  3. The rest of the panel
  4. Now the uncomfortable part
  5. When fan-in does earn a finding
  6. How to actually use these numbers

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

AFFERENT (Ca): who depends on you OrderRepository high Ca Change this → every arrow is a place that can break EFFERENT (Ce): what you depend on CheckoutService high Ce Every arrow is a reason this might be forced to change

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

High Ca · Low Ce

Stable core

Interfaces, domain types, shared contracts. Everyone depends on them; they depend on little. Healthy, but every change here is expensive by design.

High Ca · High Ce

The danger zone

Many dependents and many dependencies: god classes, "utils" dumping grounds, accidental bridges. Fragile to change, impossible to avoid.

Low Ca · Low Ce

Quiet leaf

Self-contained helpers and features. Change freely; almost nothing can break.

Low Ca · High Ce

Orchestrator

Controllers, entry points, wiring code. Volatile but safe: nothing depends on them, so their churn does not ripple.

↑ rows: afferent coupling (Ca)columns: efferent coupling (Ce) →

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

NOC: 2±0 DIT: 3±0 WMC: 38+36% ENC: 0.7+38% AC: 5±0 EC: 44+22%
BadgeWhat it measuresWhat it hints at
WMCWeighted method complexity: the summed cyclomatic complexity of the class's methodsThe best single "is this becoming a god class?" gauge. Logic pooling here instead of being distributed.
DITDepth of inheritance tree: how many ancestors the class hasDeep hierarchies make behaviour hard to trace; every layer is somewhere logic can hide.
NOCNumber of children: direct subclassesA contract many things extend. Like high Ca, it multiplies the cost of every change made here.
ENCEncapsulation ratio: the share of members that are private or protectedFalling ENC means internals are being exposed, inviting exactly the coupling the other numbers then measure.
AC / ECAfferent and efferent coupling, as aboveBlast 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

1

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.

2

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.

3

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.

4

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.

The finding is not "afferent coupling is 12". The finding is "a public method was removed from a type twelve things depend on". The number is not the claim; it is the magnitude attached to a claim that stands on its own. Delete the number and there is still a finding: a public contract shrank. Delete the contract change and there is nothing: twelve dependents is just a fact about the code, and it was true yesterday too.

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.