skip to content

Structured design ranks coupling from content coupling down to data coupling, and cohesion from coincidental up to functional. Walk through those two scales and explain how you would actually use them when reviewing a module boundary.

level: seniorimportance: should knowfreq 38%

answer

  1. Coupling worst→best: content, common, external, control, stamp, data
  2. Cohesion worst→best: coincidental, logical, temporal, procedural, communicational, sequential, functional
  3. Flag argument = control coupling; Utils class = coincidental cohesion
  4. SRP = cohesion by actor; Common Closure at component scale
  5. Scales omit direction, cycles, volatility

basics

~20 s

Coupling, worst to best: content (reaching into another module's internals), common (shared global data), external, control (passing a flag that steers the callee's logic), stamp (passing a whole record when only a field is needed), data (passing just the needed values). Cohesion, worst to best: coincidental, logical, temporal, procedural, communicational, sequential, functional (one job).

solid answer

~60 s

These are Constantine and Yourdon's ordinal scales from structured design. **Coupling** (worst→best): *content* — one module modifies or branches into another's internals; *common* — modules share global mutable data; *external* — modules share an externally imposed format or device; *control* — one passes a flag telling the other which internal path to take, so the caller knows the callee's structure; *stamp* — a whole composite is passed when only part is used, spreading knowledge of that structure; *data* — only the primitive values needed are passed. **Cohesion** (worst→best): *coincidental* (a Utils grab bag), *logical* (one entry point switching on a mode), *temporal* ("everything at startup"), *procedural* (steps in an order), *communicational* (all operate on the same data), *sequential* (output of one is input of the next), *functional* (one well-defined job). In review I use them as a shared vocabulary and a direction of travel, not a score: name the level, name the change that would hurt, and propose the specific move — replace a boolean flag with two methods, replace shared globals with explicit parameters, split a Utils class by actual client. Modern additions: message coupling (below data, via messages/events) and Page-Jones's connascence as a finer instrument.

code

pseudocode · 16 lines
pseudocode
// control coupling: caller must know the callee's internal branching
fun export(report: Report, asPdf: Boolean, compress: Boolean)
export(r, true, false)     // unreadable at the call site

// improved: named operations, choice made once
interface Exporter { fun export(report: Report): Bytes }
class PdfExporter : Exporter
class CsvExporter : Exporter

// stamp coupling: whole aggregate passed for one field
fun notify(customer: Customer) { mail.send(customer.email, ...) }
// data coupling:
fun notify(address: EmailAddress) { mail.send(address, ...) }

// common coupling: several modules read/write shared mutable state
object Session { var currentUserId: String? = null }   // who owns the invariant?

go deeper

for a junior

Know the endpoints and one example each: content coupling (reaching into internals) is worst, data coupling (pass only what's needed) is best; coincidental cohesion (Utils) is worst, functional (one job) is best.

for a middle

List both ladders in order with a concrete example per rung, and name the standard refactors — flag argument to two methods, global state to explicit parameter, Utils split by client.

for a senior

Use them as review vocabulary, always tied to a specific future change; discuss nuances (stamp coupling can be right), the logical-cohesion/control-coupling mirror, and what the scales omit (direction, cycles, volatility). Connect cohesion to SRP and Common Closure.

for a principal

Position the taxonomy as a communication tool, prefer connascence for precise arguments, and apply the scales at service/organization scale — shared database as common coupling, event contracts as message coupling — with explicit, recorded trade-offs where you accept a worse rung.

## Where the scales come from Larry Constantine and Ed Yourdon (*Structured Design*, 1970s; earlier in Stevens/Myers/Constantine 1974) proposed **ordinal scales** to make "low coupling, high cohesion" concrete enough to review. They predate objects entirely, which is why they transfer cleanly to functions, classes, packages, and services alike — nothing in them assumes a language or paradigm. ## Coupling scale (worst first) 1. **Content coupling (pathological).** Module A relies on or modifies module B's internals: writing B's private data, jumping into the middle of B, depending on B's local variable layout. Modern forms: reflection into private fields, monkey-patching, mutating an object returned by reference that the owner still uses, depending on a subclass's knowledge of a superclass's private state. *Why worst:* any internal change to B silently breaks A, and the compiler/tests may not catch it. 2. **Common coupling.** Modules share globally accessible mutable data — a global variable, a singleton holding state, a shared mutable configuration blob, a shared database table written by several modules. *Why bad:* no module owns the invariant; reasoning requires knowing every writer; concurrency and test isolation collapse; changing the shape of the shared data affects everyone at once. 3. **External coupling.** Modules are jointly bound to an externally imposed detail: a device protocol, a file format, a fixed wire layout. Unavoidable at the edges; the fix is to confine it to one adapter rather than let it spread. 4. **Control coupling.** A passes a value whose purpose is to select which internal path B takes — the classic `render(data, isPdf)` or `process(order, mode = 2)`. *Why bad:* the caller must know B's internal branching structure, so B's alternatives are not really hidden; adding a third mode changes both sides; the flag argument makes call sites unreadable. *Fix:* two named operations, or polymorphism/strategy so the choice is made once at construction. 5. **Stamp coupling (data-structure coupling).** A passes a whole composite record when B only needs one or two fields — `sendEmail(customer)` when only `customer.email` is used. *Why bad:* B is now compiled/tested against the composite's shape and may accidentally start using more of it; it also drags an unnecessary dependency (and potentially sensitive data) across the boundary. *Nuance:* passing a cohesive domain value object is often *better* than exploding it into five primitives — stamp coupling matters when the composite is incidental to the callee's job, not when it *is* the concept. 6. **Data coupling (best of the classic scale).** Only the specific primitive/value parameters needed are passed, and nothing is shared implicitly. Later writers add **message coupling** below data coupling: interaction only through messages/events with no shared types beyond the message itself. And **no coupling** at the bottom, which is only achievable for genuinely independent modules. Orthogonal factors the scale doesn't capture, which you must layer on in review: **direction** (does the dependency point toward the more stable side?), **cycles** (A↔B is far worse than either alone), **fan-out** (how many things must exist for this to run), and **volatility** of the depended-upon thing. ## Cohesion scale (worst first) 1. **Coincidental.** Elements grouped for no reason — `Utils`, `Helpers`, `Common`. Nothing predicts what's inside; every team edits it; it becomes a dependency magnet. 2. **Logical.** Elements in the same broad category, selected by a flag through one entry point — e.g., one `handleInput(kind, payload)` that internally switches on kind for mouse, keyboard, and network input. Note this is the mirror image of control coupling: logical cohesion inside creates control coupling at the interface. 3. **Temporal.** Grouped because they happen at the same time — `initialize()`, `shutdown()` that touch a dozen unrelated subsystems. The only relationship is the clock, so the module changes whenever *any* subsystem changes. 4. **Procedural.** Grouped because they execute in a certain order, but operate on different data. Sensitive to reordering and to changes in any step. 5. **Communicational (informational).** All elements operate on the same data set — e.g., all operations on a customer record. This is roughly the cohesion of a well-formed abstract data type and is a perfectly good target in practice. 6. **Sequential.** Output of one element feeds the next, all on the same data — a pipeline stage. 7. **Functional (best).** Every element contributes to one single, well-defined task; you can name the module in a verb phrase with no conjunctions. **Where SRP fits.** Robert Martin's Single Responsibility Principle — "a module should have one, and only one, reason to change", later sharpened to "be responsible to one actor/stakeholder" — is essentially a change-oriented restatement of high cohesion. The classic taxonomy asks *what relates these elements?*; SRP asks *who requests changes to them?* The second question is more actionable, because two functions that look related can still be owned by the accounting department and the operations department, and merging them guarantees conflicting change requests. At component scale the same idea becomes the **Common Closure Principle** (classes that change for the same reasons at the same times belong in the same component). ## How to actually use these in review They are diagnostic vocabulary, not a metric to maximize. A workable procedure: 1. **Name the level.** "This is control coupling" is more actionable than "this feels wrong", and less arguable. 2. **Name the change that would hurt.** "When we add the third export format, both the caller and the callee change, and every call site's boolean becomes ambiguous." The scale's ordering is a proxy; the concrete future change is the argument. 3. **Propose the specific move.** Flag argument → two named methods or a strategy. Global state → explicit parameter or injected collaborator. Utils class → split by actual client and move each function next to its user. Temporal cohesion → let each subsystem own its own lifecycle and have the orchestrator call each one. Stamp coupling → narrow the parameter to what's used, *unless* the composite is the domain concept. 4. **Weigh the cost.** Moving one step up the scale can cost readability (five primitive parameters instead of one object) or performance (copying instead of sharing). Accept a worse level deliberately and write down why. 5. **Look for the paired symptom.** Logical cohesion inside almost always shows up as control coupling outside; a split responsibility shows up as high coupling plus co-change in git history. Fixing one usually fixes the other. ## Limits The scales are coarse and ordinal — there is no arithmetic, no aggregation into a single number, and "stamp" vs. "data" arguments can become pedantic. They also ignore direction, cycles, and volatility, which often matter more than the category. **Connascence** (Meilir Page-Jones) is the finer instrument: it names the *kind* of agreement two elements share (name, type, position, meaning, algorithm, timing, execution order, identity) and adds strength, degree, and locality as separate dimensions. Use the classic taxonomy for shared vocabulary and quick triage; use connascence when you need to argue precisely about a specific coupling and its remedy.

  • Is stamp coupling always worse than data coupling?
    No. If the composite *is* the concept the callee operates on — a `Money`, an `Order`, an `EmailAddress` — passing it is clearer and safer than exploding it into loose primitives, which invites argument-order bugs and primitive obsession. Stamp coupling is a real problem when an incidental aggregate is passed for one field, dragging in a type dependency and possibly sensitive data the callee has no business seeing.
  • Why is a boolean flag parameter singled out as control coupling when it's so convenient?
    Because the caller must know the callee's internal alternatives to choose correctly, which defeats hiding; call sites become unreadable (`export(r, true, false)`); the callee grows a conditional that both sides must extend; and it usually indicates two responsibilities sharing one body, i.e. logical cohesion inside. Splitting into named operations or a strategy makes both sides simpler.
  • How do these scales apply above the class level — to services?
    Directly. Two services sharing a database table is common coupling at deployment scale, with all the same failures (no owner of the invariant, schema changes break everyone). A service passing a `type` field that switches another service's behavior is control coupling. Event/message-based interaction with an explicit, versioned schema is message coupling, the loosest form — at the cost of eventual consistency and harder debugging.
  • How does the Single Responsibility Principle relate to the cohesion scale?
    SRP is a change-oriented restatement of high cohesion: instead of asking what relates these elements, it asks who asks for them to change. Two functions can be topically related but owned by different actors; SRP says separate them, because they'll receive conflicting change requests. At component scale it becomes the Common Closure Principle.

saying these in an interview costs you the question

  • Reciting the ladders as a score to maximize instead of a diagnostic vocabulary tied to a concrete anticipated change.
  • Insisting stamp coupling must always be refactored into primitive parameters — this produces primitive obsession and argument-order bugs.
  • Treating a singleton or module-level 'config object' as harmless when it holds mutable state shared across modules; that is common coupling.
  • Missing that logical cohesion inside a module and control coupling at its interface are the same defect seen from two sides.
  • Believing the scales capture everything — they ignore dependency direction, cycles, fan-out, and volatility, which often dominate.
  • Calling a `Utils`/`Common`/`Helpers` package 'reuse'; it is coincidental cohesion and becomes a dependency magnet.

context