Skip to content
Kumar Chandrachooda
Engineering Practice

Coupling, Cohesion and the Main Sequence

Measuring the lines between the boxes - afferent and efferent coupling, the instability ratio, Robert C. Martin's main sequence, and the cohesion metric that catches a class doing two jobs, plus where every one of them needs a human to overrule it.

By Kumar Chandrachooda 27 Apr 2026 7 min read
Modules scattered around the diagonal line where architecture wants them

Every metric so far in this series stops at a file boundary. Complexity lives inside one function; the composites from part 3 grade one module at a time, in isolation, as if code kept to itself. But the structural failures that actually sink codebases are social: the assembly that everything depends on and nobody dares touch, the module that reaches into nine others and breaks whenever any of them sneezes, the class quietly doing two unrelated jobs. Those problems live in the lines between the boxes, and measuring them takes a different toolkit — most of it laid out by Robert C. Martin in a 1994 paper, “OO Design Quality Metrics”, whose ideas later became a centrepiece of Clean Architecture. This part walks that toolkit, plus the cohesion metric from the same era, and finishes with the part the papers cannot supply: knowing when the numbers are lying to you.

Two directions of dependence

Start with the raw counts, because everything else is arithmetic on top of them. Picture an invented order-fulfilment solution, Dispatch, with a handful of assemblies: Dispatch.Contracts holding the shared interfaces and message types, Dispatch.Pricing and Dispatch.Routing holding business logic, Dispatch.Persistence wrapping the database, and a delivery-facing Dispatch.Api on top.

Afferent coupling (Ca) counts inward dependence: how many types outside a module depend on types inside it. In Dispatch, the Contracts assembly has high Ca — everything references its interfaces. High Ca is neither praise nor accusation; it is a statement of blast radius. A change to a high-Ca module ripples outward into every dependent, which is why such modules need to be the most stable, most carefully tested code you own.

Efferent coupling (Ce) counts the opposite direction: how many external types a module depends on. Dispatch.Api has high Ce — it reaches into pricing, routing, persistence and contracts to do its job. High Ce is a fragility statement: the module sits downstream of many change sources, any of which can break it, and it tends to resist isolated testing for the same reason.

Neither number means much alone. Core domain modules naturally accumulate Ca; orchestration layers naturally accumulate Ce. The trouble starts when the two counts combine badly — which is exactly what Martin's next move measures.

Instability is a ratio, not an insult

Martin's instability metric folds both directions into one number:

I = Ce / (Ce + Ca)

The ratio runs from 0 to 1. At I = 0 the module depends on nothing and is depended on heavily — completely stable, in the sense that it has every reason to stay put and enormous cost attached to moving. At I = 1 the module depends on others and nothing depends on it — completely unstable, free to change tomorrow because nobody downstream would notice. Despite the loaded words, neither end is good or bad. An architecture needs both: stable foundations and unstable, easily changed edges. Dispatch.Contracts should sit near 0; Dispatch.Api should sit near 1; and the useful discipline — Martin's stable-dependencies principle — is that dependencies should point in the direction of stability, so that volatile modules depend on steady ones and never the reverse. A stable module that depends on an unstable one has coupled its enormous blast radius to someone else's freedom to change on a whim.

The diagonal where modules belong

Instability alone leaves a puzzle. If stable modules are expensive to change, and business rules must keep changing, how does the core of a system stay both depended-upon and flexible? Martin's answer is the second axis: abstractness, the proportion of a module's types that are abstract — interfaces and abstract classes rather than concrete implementations. An abstract module can be extended without being edited; new behaviour arrives as new implementations while the contracts everyone depends on hold still.

Plot every module with instability on one axis and abstractness on the other, and Martin's ideal appears as a diagonal line from (0, 1) to (1, 0) — the main sequence, borrowing its name from astronomy. Modules belong near that line in one of two healthy postures: stable and abstract, like Dispatch.Contracts — heavily depended on, made safe by being made of interfaces — or unstable and concrete, like Dispatch.Api — full of implementation detail, harmless because nothing depends on it. The two corners far from the line are where architecture goes to hurt. Stable and concrete — the bottom-left — is the zone of pain: a module full of implementation that everything references, so every change is both likely and expensive. Unstable and abstract — the top-right — is the zone of uselessness: interfaces nobody implements or depends on, abstraction spent where it buys nothing.

The distance from any module to the diagonal compresses into a single number:

D = |A + I - 1|

Zero means on the sequence; values near one mean deep in a bad corner. It is, I think, the most underrated number in the whole structural-metrics catalogue — and also one that needs careful handling, which is the next section's job.

What the distance buys a reviewer, and where it lies

Here is what I actually do with these numbers, offered as my practice rather than industry law. Quarterly, alongside the debt-ratio trend from part 3, I look at a per-assembly plot of A against I and read it as a question generator, never a verdict. A module drifting toward the zone of pain over two or three quarters — concrete types accumulating in an assembly whose dependents keep multiplying — is the single best early warning I know for “this will be unrefactorable next year”. In review, the same thinking works without any tooling at all: when a pull request adds a reference from a low-instability assembly to a high-instability one, that arrow now points against stability, and it is the most consequential line in the diff however innocent it looks. Catching that dependency at review time costs one comment. Catching it two years later costs a migration project.

Now the lying. First, these metrics only earn their keep at architecture level — per module, per assembly, per package. Applied per class they mostly generate noise; the sheer number of points swamps the signal, and class-level dependency counts are dominated by mechanical references that carry no design meaning. Second, the zone of pain has permanent residents that belong there: an assembly of plain data types, or a battle-tested utility library, can be concrete, heavily depended on — and entirely harmless, because it never needs to change. Distance from the main sequence measures exposure to a problem; it becomes a problem only when the module also churns, which is why part 6 of this series pairs structural metrics with change history. And third, high Ca on your core domain is the system working as designed, not a finding. Every one of these numbers requires domain context to interpret; a dashboard that flags Dispatch.Contracts for being depended upon has understood nothing.

The class doing two jobs

Coupling watches the lines between modules; cohesion watches whether the things inside one box belong together. The classic instrument is LCOM — lack of cohesion of methods — from Shyam Chidamber and Chris Kemerer's 1994 metrics suite, with the variant you will most often meet, LCOM-HS, refined by Brian Henderson-Sellers in 1996 and running on a scale from 0 to 2. The intuition: a class's methods should mostly work with the same instance fields. When they instead split into camps that touch disjoint sets of fields, the class is two classes sharing a name. Consider:

public class CustomerAccountService
{
    private readonly IAccountRepository _accounts;
    private readonly IEmailSender _email;
    private readonly IReportWriter _reports;
    private readonly IClock _clock;

    public void Register(Customer customer)   { /* uses _accounts, _email */ }
    public void Close(AccountId id)           { /* uses _accounts, _email */ }
    public void ExportMonthlyStatement()      { /* uses _reports, _clock  */ }
    public void ArchiveOldStatements()        { /* uses _reports, _clock  */ }
}

Two methods orbit _accounts and _email; two orbit _reports and _clock; no method crosses the divide. LCOM sees exactly that fracture, and a high score here reads as the single-responsibility principle failing in a measurable way — the account-lifecycle half and the reporting half want to be separate classes with separate reasons to change. My own alarm line is an LCOM-HS above 1.0, and I hold that as a convention worth investigating, not a law of nature. The metric's blind spots are structural: data-transfer objects, facades and deliberately grab-bag utility classes all score terribly while doing precisely the job asked of them. Like every number in this part, it locates a suspect; the interview is still yours to conduct.

The seam to the next part

Between them, today's metrics describe how a system is organised: which modules bear weight, which direction the dependencies flow, which classes have quietly become two. What none of them can say is whether the tests wrapped around all that structure would actually notice a break — and that question has the most gamed answer in the entire metrics catalogue. Next: coverage is not testing.