Software Architecture · Modularity
Coupling and Cohesion as Independent Structural Axes
Why dependency strength between modules and relatedness inside a module are separate measurements, and how the classic coupling and cohesion scales describe them.
Coupling and cohesion are usually introduced together, in one sentence, as though they were two halves of a single instruction: minimise the first, maximise the second. They are not symmetric. Coupling is a property of an edge in a dependency graph; cohesion is a property of a partition of the vertices. A change that improves one can worsen the other, and a system can score well on both while remaining hard to change, because neither metric observes time.
The distinction matters operationally. Coupling predicts what breaks when a module changes. Cohesion predicts whether a change to one requirement stays inside one module or sprays across several. Both are statements about change, not about code layout, which is why counting imports and measuring directory sizes captures so little of either.
Coupling is a count of assumptions
Two modules are coupled to the degree that one must know something about the other’s internals in order to compile, run, or remain correct. The classic scale, originating with Stevens, Myers, and Constantine and later extended with external coupling, orders coupling by the kind of knowledge involved, from least to most damaging:
- No direct coupling. Modules communicate only through a caller that mediates, or not at all.
- Data coupling. One module passes a scalar or a well-defined record to another. The callee uses only the values it is given.
- Stamp coupling. A whole structure is passed when only some fields are needed, so the callee’s behaviour can silently depend on fields it never reads.
- Control coupling. One module passes a flag that selects a branch inside another, making the caller responsible for the callee’s internal control flow.
- External coupling. Both modules depend on the same externally imposed format or interface — a file layout, a wire protocol, a device register map.
- Common coupling. Both modules read and write the same global or shared mutable state.
- Content coupling. One module reaches directly into another’s representation, for example by mutating a field that the owner considers private.
The ordering is not arbitrary. Each step adds knowledge that the compiler and the type system cannot helpfully check, and that a reader of the calling module cannot see. Data coupling is inspectable at the call site: the arguments are the contract. Content coupling is not visible at the call site at all, because the violation is the absence of a boundary rather than the presence of an argument.
A flag parameter is the standard example of control coupling, and it is worth seeing why the same two characters can be benign in one place and corrosive in another:
def render(node, expanded: bool):
if expanded:
return render_subtree(node)
return render_leaf(node)
At a call site, render(n, True) tells the reader nothing about what will happen; the meaning of True lives in the callee, and adding a third mode turns the boolean into an enum that every caller must re-examine. Splitting into render_subtree and render_leaf moves the branch to the caller, where the choice is explicit and where callers that always pass the same value stop paying for the conditional. The flag was not wrong because it was a boolean. It was wrong because it encoded a decision that belonged one level up.
Common coupling via a mutable module-level registry is more subtle, because the dependency is invisible in signatures. Any function that reads the registry can be affected by any function that writes it, including functions in other modules, and the effect depends on execution order. The number of potential interactions grows quadratically with the number of writers and readers, which is the mechanism behind test suites that pass individually and fail together.
Cohesion is a claim about why things belong together
Cohesion asks a different question: given that these declarations are in the same module, what holds them there? The traditional scale runs from functional cohesion, where every element contributes to one task, through sequential and communicational cohesion, down to logical and coincidental cohesion, where elements share nothing but a name or a file.
Functional cohesion is the ideal because it means the module has one reason to exist, and therefore roughly one reason to change. Communicational cohesion — several operations over the same data — is weaker but often defensible: a repository that only reads and writes one aggregate has a real invariant tying its methods together. Coincidental cohesion is the failure mode that produces modules called utils, helpers, or common, where the only relationship between members is that someone needed them and did not know where else to put them.
The practical test is not what the module contains but what changes together. A module with three functions that never change for the same reason is three modules sharing a directory. A module with two functions that must always be modified in the same commit, because they encode two views of one rule, is cohesive even if the functions look unrelated in shape.
This is where static cohesion metrics diverge from the property that matters. A metric based on shared types or shared field access will call a data-transfer-object-heavy module cohesive when it merely centralises boilerplate. A metric based on call graph density will call a module cohesive when its functions call each other, which is common in accidental god objects. Neither observes the commit history, which is the only record of which elements actually move together.
The axes are genuinely independent
Consider the four combinations. A module tightly coupled to one other module can be internally cohesive: think of a parser and a lexer split into two files that pass one token stream, where the parser has a single job. A module loosely coupled to everything can be internally incoherent: a configuration module that exposes fifty unrelated getters, imported by everyone but depending on nothing. High cohesion with high coupling is the normal state of a well-factored but tightly scoped subsystem; low cohesion with low coupling is the normal state of a distributed monolith’s leftovers.
Because the axes are independent, “reduce coupling” is not a complete instruction. Every decomposition replaces internal dependencies with cross-module dependencies. If a function is moved out of a module and called from its old home, the edge still exists; only its visibility changed. Splitting a 2,000-line file into ten files does not reduce the number of calls, and if the new modules import each other in both directions the number of edges may rise. What it can reduce is the amount of internal state each module must be reasoned about together with, and that is the actual coin being spent.
The cost side of the ledger is interface surface. Each new boundary introduces parameters, types, and error conventions that must be stable across it. A boundary with one caller and no independent lifecycle is usually pure overhead: the indirection is paid on every call and the abstraction pays nothing back. This is why decomposition depth should follow variation, not aesthetics.
Measuring both without fooling yourself
Afferent coupling (fan-in) counts the modules that depend on a given module; efferent coupling (fan-out) counts what it depends on. Their sum is a rough proxy for how much a change to that module can disturb, and how much disturbance it absorbs. A module with fan-in of 40 is a change multiplier: every edit requires inspecting 40 dependents, whether or not they break, because the reviewer cannot know which ones rely on the altered behaviour. Fan-out of 40 describes a module that is expensive to test and likely to be a coordinator with little logic of its own.
Both numbers are derived from the graph and therefore blind to the strength of each edge. Thirty data-coupled imports are not the same risk as three content-coupled imports, but a raw import count treats them identically. The corrective is to classify edges when they are created: an import of a type used only in a signature is weaker than a call into a function that mutates shared state. Some build systems expose this distinction; most do not, so the classification has to happen in review.
Cohesion is harder to instrument and easier to fake. The honest signals are change coupling — files that are modified in the same commits more often than chance would predict — and the size of the smallest coherent unit that can be tested. A module whose every test requires constructing four collaborators is signalling that its content does not belong together, regardless of what its name claims.
Where the model breaks
The framework assumes modules have stable identities. In practice a module may be a file, a package, a deployable service, or a team, and coupling at one level does not imply coupling at another. Two packages can be data-coupled and still be deployed together, which makes the boundary decorative. Conversely, a single module can contain a seam that behaves like a hard boundary for testing and a soft one for refactoring. Deciding which level a given analysis refers to is a prerequisite for using either term; the same code can be exemplary at the package level and catastrophic at the service level.
The other limitation is that both metrics are static descriptions of a structure that exists to absorb change. They say nothing about the direction of likely change, which is the only thing that determines whether a boundary will hold. Parnas’s criterion — hide the decisions most likely to change — supplies that missing dimension, and it is the reason information hiding is treated as the design principle while coupling and cohesion are treated as its measurements. A boundary drawn around a volatile decision will stay cheap even when the graph looks dense; a boundary drawn through the middle of one will rot regardless of how few arrows cross it.
References
- Structured Design — W. P. Stevens, G. J. Myers, L. L. Constantine, IBM Systems Journal 13(2), 1974
- On the Criteria To Be Used in Decomposing Systems into Modules — D. L. Parnas, Communications of the ACM 15(12), 1972
- The Practical Guide to Structured Systems Design — Meilir Page-Jones, Prentice Hall, 1988