skip to content

The classic coupling scale runs from content coupling (worst) to data coupling (best). Walk through the levels and explain why content and common coupling are considered the most dangerous.

level: middleimportance: must knowfreq 58%

answer

  1. Content → Common → External → Control → Stamp → Data → Message
  2. The two "Co-" levels are the worst
  3. Control = flag steers callee's branches
  4. Stamp = whole record, two fields needed
  5. Shared DB table = common coupling

basics

~20 s

Worst to best: content (one module touches another's internals), common (shared global data), external (shared external format/device), control (a flag tells the callee what to do), stamp (passing a whole record when only a field is needed), data (passing only the simple values needed). Content and common are worst because a change anywhere breaks everything silently.

solid answer

~60 s

**Content coupling**: module A reaches inside B — modifies its private fields, jumps into its middle, or depends on its memory layout. Any internal change to B breaks A, and the compiler often cannot warn you. **Common coupling**: several modules read and write the same global mutable state; you cannot reason about any one of them in isolation, ownership of the data is undefined, and concurrency bugs follow. **External coupling**: modules share an externally imposed format, protocol or device (a file layout, a device register); acceptable but changes propagate to everyone at once. **Control coupling**: A passes a flag that dictates B's internal control flow — the caller must know B's internals, and adding a mode changes both sides. **Stamp coupling**: A passes a whole structure when the callee needs two fields — the callee is now coupled to the shape of a record it barely uses. **Data coupling** (best): only the simple values actually needed are passed. Beyond that, message coupling — communicating only through a narrow published interface or an event — is the weakest form.

code

pseudocode · 11 lines
pseudocode
// CONTROL + STAMP coupling: caller steers the callee, passes a whole aggregate
formatAddress(customer /* full record */, shortForm /* flag */)

// DATA coupling: only what is needed, intent visible at the call site
formatShortAddress(street, city, postalCode)
formatFullAddress(street, city, postalCode, country)

// COMMON coupling: two modules bound through shared mutable state
global CURRENT_USER
module Billing  { charge() { use(CURRENT_USER) } }
module Auditing { log()    { CURRENT_USER = null } }   // silently breaks Billing

go deeper

for a junior

Name the two ends — content coupling (touching another module's internals) is worst, data coupling (passing just the values needed) is best — and give one example of each.

for a middle

List all levels in order with an example each, and explain that control coupling means a flag steering the callee while stamp coupling means passing a whole record for a couple of fields.

for a senior

Argue in terms of change propagation and visibility: why common coupling has no owner and no compiler check, when external coupling is acceptable behind an adapter, and how to refactor control/stamp coupling.

for a principal

Move up a level: shared-database common coupling across services, temporal/runtime coupling and availability math, contract versioning, and connascence as the sharper modern vocabulary (reduce strength, degree, locality).

## Why a scale Coupling is the degree to which one module depends on another. It is ranked by **how much a change in the target forces a change in the dependent**, and by **how visible that dependency is to tools and reviewers**. Invisible dependencies are the expensive ones, because nothing tells you when you have broken them. ## The levels, worst to best ### Content coupling (a.k.a. pathological coupling) — worst Module A depends on the *internal implementation* of module B: it mutates B's private field, relies on B's data layout, patches B's code, or branches into the middle of B's procedure. Modern equivalents: reflection into private state, monkey-patching another library, casting to a concrete implementation type to reach a method not on the interface, depending on undocumented behaviour of an internal function. *Why worst:* B's author has no way of knowing the field is load-bearing. A pure refactor of B — renaming a private field, reordering initialisation — breaks A at runtime, often silently and far from the cause. Encapsulation is defeated, so no reasoning about B in isolation holds. ### Common coupling (global coupling) Several modules share access to the same **global mutable data**: a global variable, a singleton holding state, a shared mutable cache — and, at architecture scale, **a database table written by multiple services**. *Why nearly as bad:* ownership is undefined, so no module can enforce an invariant on the data; who wrote a bad value is unknowable without whole-system tracing; adding a field or changing its meaning affects every reader; and concurrent access creates race conditions. It is also *transitive by stealth* — module A and module Z have a dependency neither author has ever seen. ### External coupling Modules depend on an externally imposed representation: a file format, wire protocol, hardware device register, or an operating-system API. This is often unavoidable and legitimate — but it means a format change hits every participant simultaneously, so the usual mitigation is to isolate the format behind a single adapter/anti-corruption module that everyone else goes through. ### Control coupling A passes a value whose only job is to select B's internal control path: `render(data, isDraft)`, `save(x, mode = 2)`. The caller must understand B's internal branching to call it correctly, and a new mode changes both the callee and every caller. It also usually indicates that B has low (logical) cohesion. Standard fix: split into distinct, well-named operations, or accept a strategy/polymorphic collaborator instead of a flag. A notable sub-case is the **boolean flag argument**: at a call site, `publish(article, true)` conveys nothing without opening the callee. ### Stamp coupling (data-structure coupling) A passes a composite structure to B when B only needs part of it: passing the whole `Customer` aggregate to a function that reads only `postalCode`. Now B compiles against the shape of `Customer`, so unrelated changes to that record can break or recompile B, B is harder to test (you must construct a full object), and B has been handed data it should not see — which is also a least-privilege concern. ### Data coupling — best of the classic levels A passes exactly the primitive/simple values B needs, and B returns a result: `taxFor(amountCents, jurisdictionCode)`. The dependency is minimal, visible in the signature, and easy to test. ### Message coupling / no coupling (modern addition) Communication only through a narrow public interface with no shared state — or fully decoupled via asynchronous events where the publisher does not know its subscribers. Weakest coupling available, though it trades away compile-time checking and adds eventual-consistency and choreography-tracing costs. ## Mnemonic order **Content → Common → External → Control → Stamp → Data → Message.** (Remember at least that the two "Co-" levels are the bad ones and that Data is the good end.) ## Sharpening the classic scale The 1970s scale predates OO and misses cases, so two modern refinements are worth knowing: - **Afferent/efferent coupling** (Ca/Ce): counting *directions* of dependency rather than kinds — how many modules depend on me versus how many I depend on. - **Connascence** (Meilir Page-Jones): classifies coupling by what must change together — name, type, meaning, position, algorithm (static forms) and execution order, timing, value, identity (dynamic forms). Its rules — reduce the *strength*, *degree* and *locality* of connascence — give a more actionable vocabulary than the old scale, and it explicitly says stronger connascence is tolerable when it is *local* (inside one small module). ## Edge cases - **Not all coupling should be removed.** Depending on a stable, versioned contract is cheap. Aggressively eliminating a dependency by introducing an interface with exactly one implementation adds indirection without reducing real change-propagation. - **Loose coupling can hide coupling.** Passing an untyped map or a giant JSON blob "to decouple" trades a compile-time dependency for an invisible runtime one — the fields are still a contract, just an unchecked one. - **Temporal/runtime coupling** — service A cannot serve a request unless B is up right now — is not on the classic list but is often the most damaging kind in distributed systems; asynchronous messaging is the usual remedy.

  • Is a shared database between two services content coupling or common coupling?
    Common coupling: both modules read and write the same shared mutable data with no owner enforcing invariants. It shades into content coupling if one service depends on internal columns, triggers, or table layout the owning service considers private.
  • Why is a boolean flag parameter considered a design smell?
    It is control coupling: the caller must know the callee's internal branching, the call site (`publish(x, true)`) is unreadable, and the callee usually has logical rather than functional cohesion. Splitting into two named operations, or passing a strategy, removes both problems.
  • Does passing a large DTO always count as stamp coupling worth fixing?
    Not always. At a module boundary, a stable published contract object can be clearer than a twelve-parameter signature. It is worth fixing when the callee needs only a couple of fields, when the structure is volatile, or when handing over the whole record leaks data the callee should not see.

Content coupling is reaching into your neighbour's house to move their furniture; common coupling is everyone on the street sharing one unlocked shed; control coupling is telling the chef which pan to use; stamp coupling is handing over your whole wallet when the fare is exact change; data coupling is handing over just the coins.

saying these in an interview costs you the question

  • Calling any parameter passing 'data coupling' — a flag that steers the callee's control flow is control coupling regardless of its type.
  • Thinking a shared database is not coupling because there is no code-level import.
  • Claiming all coupling should be eliminated; depending on a stable versioned contract is cheap and necessary.
  • Passing an untyped map or JSON blob and calling it decoupled — the fields are still a contract, just unchecked.
  • Forgetting runtime/temporal coupling (B must be up right now), which the classic scale never names but which dominates distributed systems.

context