open atlas
↑ Back to track
Architecture Patterns ARCH · 01 · 01

Coupling forms

Afferent and efferent coupling measure dependency flow in and out of a module. The classic coupling ladder runs content→common→control→stamp→data. Looser coupling is not always better — the right level depends on the forces.

ARCH Middle ◷ 22 min
Level
FoundationsJuniorMiddleSenior

The billing team at a B2B SaaS company needed to change the tax calculation logic. Simple change, they thought — one file, maybe two hours. Instead it took three days. The tax function had been reading directly from the order module’s internal database rows, interpreting the raw column layout, and branching on an internal status integer that meant different things in different subsystems. By the time they found all the places that depended on the old behavior, they had touched eleven files and introduced two regressions. Nobody had designed this coupling intentionally. It had grown one small dependency at a time, and nobody had measured it.

Afferent and efferent coupling

Every module in a system has two coupling measures that matter separately:

Afferent coupling (Ca) is the count of modules that depend on this module — how many other parts of the system import or call it. A module with high afferent coupling is widely used. It has high stability pressure: if it changes, many things break. The billing engine in the B2B order platform has high afferent coupling — the order service, the finance dashboard, the admin tool, and the ERP integration all depend on it.

Efferent coupling (Ce) is the count of modules this module depends on — how many other parts of the system it imports or calls. A module with high efferent coupling is brittle: it is sensitive to changes in many other modules. A module that imports the order repository, the tax service, the discount engine, and the payment gateway is highly efferent. Any change to any of those four cascades into it.

The ratio Ce / (Ca + Ce) gives a rough instability score (Robert Martin’s metric). A module with Ce = 8 and Ca = 2 has instability ≈ 0.8 — it depends on many things and is depended on by few, so it absorbs shocks rather than transmitting them. A module with Ce = 0 and Ca = 20 is maximally stable — nothing it depends on can break it, but everything that uses it is exposed to its changes.

The coupling ladder

Not all coupling is equal. Page-Jones (1988) and Yourdon & Constantine defined a classic taxonomy that ranks coupling forms from tightest to loosest, like rungs on a ladder. The looser the coupling, the more independently the two modules can evolve.

Content coupling (tightest): Module A directly accesses the internal data structures or implementation details of module B. The tax function in the opening story was reading raw database rows from the order module — that is content coupling. Any change to the order module’s internal layout breaks the tax function. The coupling is invisible at the interface level; it exists in code that bypasses the interface entirely.

Common coupling: Both modules share a global mutable variable or a shared data store. Two services sharing a single database table and both writing to it directly is common coupling. Either service can corrupt the shared state in ways that break the other, and neither controls the data contract.

Control coupling: Module A passes a flag or code to module B that controls module B’s behavior. processOrder(order, mode: 'dry-run' | 'live') is control coupling if the billing engine branches on the mode flag. The caller must know about the callee’s internal execution paths — the coupling is in the control flow, not just the data.

Stamp coupling (also called data-structure coupling): Module A passes a large data structure to module B, and module B only uses part of it. Passing the full Order object when billing only needs order.items and order.customerId is stamp coupling. The modules are coupled to the shape of the shared structure even for fields neither of them should care about.

Data coupling (loosest): Module A and module B communicate only through simple, well-defined parameters — exactly the data needed, no more. The billing engine accepts (items: LineItem[], customerId: string) and returns (invoiceId: string, amount: Money). Neither module knows anything about the other’s internals. This is the gold standard.

Why this works

Why does the order of the ladder matter? Because the tighter the coupling, the more coordination is required when either module changes. Content coupling means any internal refactoring in module B risks breaking module A. Common coupling means any write to the shared state by A can corrupt B’s reads in ways that are invisible until runtime. Control coupling means A must be updated when B adds new execution modes. Stamp coupling means both modules are coupled to a shared data shape that may grow or change for reasons neither controls. Each step down the ladder reduces the blast radius of a change in one module on the other.

Why looser is not always better

Here is the structural trap: teams that understand the coupling ladder sometimes conclude “data coupling everywhere, always.” That is wrong, and the failure mode is fragmentation.

Consider the order service in the B2B platform. The Order aggregate has header, lineItems, pricing, taxDetails, approvalState, and auditTrail. If every internal function that touches order data is refactored to accept only the minimal primitives it needs, you end up with:

  • updateApprovalState(orderId, state, actorId, timestamp) — data coupled, clean.
  • recalcPricing(orderId, lineItems, customerId, discountCode) — data coupled, clean.
  • computeTax(lineItems, shippingAddress, taxClassification) — data coupled, clean.

Now coordinating a change that touches approval state, pricing, and tax together requires an orchestrator that calls all three functions, collects their results, and reassembles the updated order. The orchestrator is now tightly coupled to all three — it knows the full lifecycle of an order. You have moved the coupling up the call stack, not removed it.

The question is not “is this loosely coupled?” but “is the coupling in the right place, and does it reflect the actual invariants that must change together?” High cohesion (which we examine in the next lesson) is the counterforce. A well-cohesive module keeps things together that change together, which naturally limits the coupling to the module’s boundary rather than distributing it across callers.

lesson.inset.note

The practical heuristic: look at what changes together in production. If every time the billing team ships a change to invoice generation they also have to touch the order module’s data structures, the coupling is wrong regardless of the ladder position. Coupling is a force, not a target — the goal is coupling that matches the actual change boundaries of the domain.

Quiz

The billing engine reads order records directly from the database table owned by the order service, interpreting raw column values. After the order team renames a column and adds a nullable field, billing silently starts producing wrong invoices. What form of coupling caused this?

Quiz

The BillingEngine has Ca = 12 (twelve modules depend on it) and Ce = 1 (it depends only on the PaymentGateway interface). What does this tell you about its stability, and what does that imply for how you should treat its interface?

Quiz

A team refactors the billing engine so that every function accepts only the minimal primitives it needs, achieving data coupling throughout. The billing engine now calls twelve separate functions to process an order. Debugging a billing discrepancy requires tracing through all twelve. What is the real structural problem?

Recall before you leave
  1. 01
    What is afferent coupling, and why does high afferent coupling make a module stable?
  2. 02
    Walk through the coupling ladder from tightest to loosest, with one sentence per rung.
  3. 03
    Why does achieving data coupling everywhere not eliminate coupling?
Recap

Coupling is measured in two dimensions. Direction: afferent coupling (Ca) counts how many modules depend on this one; efferent coupling (Ce) counts how many this one depends on. A module with high Ca and low Ce is stable — it absorbs little change from its environment but propagates widely when it changes. The instability metric Ce/(Ca+Ce) quantifies this.

Kind: the coupling ladder runs from content coupling (accessing another module’s internals directly, the tightest and most brittle) through common coupling (shared mutable state), control coupling (passing execution-controlling flags), and stamp coupling (oversized data structures) down to data coupling (minimal, precise parameters — the loosest form). Each step down the ladder reduces the scope of what changes together when one module evolves.

The trap is concluding that data coupling everywhere is always better. Loose coupling at each pairwise interface is good; over-decomposition that distributes coordination logic across an implicit orchestrator is not. The right coupling level is the one that matches the actual change boundaries of the domain — which is exactly what high cohesion, the subject of the next lesson, helps you identify.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.