Coupling
Coupling is the degree to which one part of a system depends on another, and the word is what lets a team reason about whether a change in one place will be felt in another.
What It Is
Coupling is the degree to which one part of a system depends on another. Two functions, modules, services, or teams are coupled when a change to one requires understanding or changing the other. The relationship is a matter of degree, not a binary: a function that reads a single integer from another module is coupled to it weakly; a function that reaches into another module’s internal data structure and mutates it is coupled to it tightly. The word is the measurement, not the verdict.
Coupling lives on a hierarchy that practitioners have been refining since the 1970s, from loosest to tightest:
- Data coupling. Parts share only simple data through parameters and return values. A function takes an integer and returns a string; nothing else passes between them. Loosest and safest.
- Message coupling. Parts communicate through messages or events without direct calls. The sender doesn’t know which receivers exist, and the receivers don’t know which sender produced the message. Loose, and pleasant to test.
- Interface coupling. Parts depend on a defined contract — a function signature, a protocol, a published schema — but not on the specific implementation behind it. You can swap the implementation without touching the caller, as long as the interface holds.
- Implementation coupling. Parts depend on the internal details of another part: its data layout, its private helpers, the order in which it does its work, its undocumented side effects. Tightest and most fragile, because every internal change becomes a potentially-breaking external change.
Three distinctions are worth keeping in vocabulary alongside the hierarchy. Visible coupling is the kind you can see by reading the code: explicit imports, function calls, declared dependencies. Hidden coupling is the kind that lives in shared global state, implicit ordering assumptions, or “this only works because that other module happens to set the cache first.” The visible kind is bounded; the hidden kind is what surprises you. And coupling at a distance is the one teams forget to count when they redraw their architecture: two modules that don’t reference each other directly but both depend on a third.
In agentic coding the same vocabulary applies, with the stakes adjusted upward. An agent’s mental model of a codebase is reconstructed from whatever fragments the agent can read in its context window. Coupling that’s visible (an explicit import, a function call, a declared interface) is coupling the agent can follow. Coupling that’s hidden (a global flag set in a sibling module, an implicit invariant the existing tests don’t enforce) is coupling the agent will routinely miss. The hierarchy isn’t just a structural-quality measure for human readers anymore; it’s a measure of how safely an agent can edit one part of a system without breaking another.
Why It Matters
Software is interconnected, and the cost of that interconnection isn’t proportional to the number of parts. It’s proportional to how those parts are wired together. Two functions that share a single integer can each be modified independently; two functions that share a mutable global structure are effectively one function in two files. A team that doesn’t have the word “coupling” describes each incident as a one-off (“the search feature broke the cart”) rather than as an instance of a recurring class with a known shape.
Naming the class is what makes the response designable. A team fluent in coupling doesn’t argue case-by-case about whether to extract an interface, hide an implementation detail, or move a piece of shared state behind a service. The vocabulary collapses dozens of micro-decisions into a single question: does this change push us up the hierarchy or down it? Pushing down (toward data coupling, toward stable interfaces) is the default direction; pushing up is allowed but should be deliberate and named.
There’s a second-order effect on how a system evolves. Tightly coupled systems resist change in proportion to how much of themselves you’d have to understand to change them safely. Refactoring stalls because the blast radius of any edit is unknown. Testing stalls because nothing can be tested in isolation. Parallel work stalls because two engineers (or two agents) editing nearby code keep colliding. The word “coupling” is what lets a team diagnose all of this as a single symptom of a single property, rather than as a string of unrelated frustrations.
For agentic workflows the diagnostic urgency is sharper. The rate of edits goes up when agents are doing the writing, and the rate of regressions scales with edits unless the system’s coupling is bounded. An agent given a tightly coupled module will routinely “fix” one site and break four others, because the four others were depending on a detail the agent didn’t see. A team that pushes the system down the hierarchy isn’t just buying itself easier maintenance; it’s buying itself an agent that can be turned loose on one module without it reaching out and breaking the others.
How to Recognize It
You’re looking at high coupling when changes don’t stay where you put them. A few concrete signs:
- Ripple-change effects. A one-line change to module A requires changes to modules B, C, and D before the build passes or the tests stay green. The modules were “separate” in name only.
- Test-isolation difficulty. A test for one component can’t run without spinning up half the system. The component has dependencies it didn’t declare, or it relies on global state that needs to be primed.
- The “one tug, many things move” feeling. Renaming a function turns into a thirty-file diff. Reordering two operations changes results in a module that supposedly doesn’t depend on either. Refactoring stalls every time it touches the same handful of modules.
- Implementation knowledge leaking across boundaries. A caller knows the data layout of a callee, the order in which a service does its work, the format of an internal error code. The caller and callee are nominally separated by an interface, but the interface isn’t really doing its job.
- Shared mutable state. Two modules read and write the same dictionary, cache, or session store. They’re coupled through that store whether or not they call each other directly. Coupling at a distance.
- Cross-team blocking. Two teams that “own different services” find themselves coordinating every release because changes on one side keep breaking the other. Coupling has escaped the system into the org chart.
- The agent reads farther than it edits. An agent asked to change one function ends up loading five other files to figure out whether the change is safe. The function looks small; its coupling surface isn’t.
A few coupling-prone patterns are worth recognizing in their own right:
- Singletons and globals. Anything reachable from everywhere is, by definition, coupled to everything. Convenience now, hidden coupling later.
- Inherited state. Deep inheritance chains where the subclass depends on the superclass’s internal layout — change the parent, the children break in ways the parent’s tests don’t catch.
- Schema-shaped APIs. An RPC or REST endpoint that returns the database row largely unchanged couples every consumer to the database schema, with no insulation layer.
- Implicit invariants between modules. Module A “happens to” call module B first, so module B can rely on the cache being warm. Nothing enforces the order. One refactor away from a hard-to-trace bug.
The honest test for coupling is: if I had to change this one part, which other parts would I have to read first to be confident the change is safe? Count them. If the count is small, coupling is bounded. If the count keeps growing as you investigate, coupling is the issue.
How It Plays Out
A web application stores user preferences in a global dictionary that multiple modules read and write directly. The format is implicit: a key called prefs.notify happens to be a boolean in some code paths and a dictionary in others, and the difference is held together by which module ran first. When the team tries to migrate to a typed preference format, every module that touched the dictionary breaks, including a few the team had forgotten were touching it. After the dust settles, preferences move behind a PreferenceService with a typed contract. The coupling shifts from implementation to interface, and the next format change becomes a one-file edit instead of a system-wide search.
A platform team announces that its authentication service is moving to a new identity provider. The migration was supposed to take a sprint; it takes a quarter. The reason isn’t the migration itself. The old service’s response shape had leaked into thirty-odd consumers over the years, and each consumer had grown its own ad-hoc handling of a field that wasn’t documented as part of the interface. The team is paying interest on coupling it didn’t know it had taken on. The fix isn’t the migration; it’s the introduction of an authentication client library that pins the consumer-facing shape and lets the underlying provider change behind it.
An AI agent is asked to swap out a payment provider. In a system at the interface-coupling layer, the agent rewrites the implementation behind the PaymentGateway interface, runs the existing tests, and the change stays local. In a system at the implementation-coupling layer, the agent discovers that payment provider details have leaked into the order processing module, the email templates, the admin dashboard, and a half-dozen scripts that don’t appear in any module manifest. The agent can do the work, but it now needs to read all of those before editing any of them, and the change that was supposed to be small becomes large by the second hour. The system’s coupling, not the agent’s competence, is what set the size of the job.
“The payment provider details have leaked into the order processing module and the email templates. Refactor so that all payment logic lives behind the PaymentGateway interface and nothing else references Stripe directly.”
Consequences
When a team has the vocabulary of coupling and uses it deliberately, the system stops surprising them. Changes stay where they’re put. Tests run in isolation. Two engineers (or two agents) can work on adjacent modules without their edits colliding. The team can answer the question “what’s the blast radius of this change?” without having to read the whole repo first.
The honest tradeoffs are worth naming, because pushing coupling down the hierarchy isn’t free.
- Indirection has costs. Every interface, message queue, or event bus you introduce to decouple two parts is one more layer for a human or agent to traverse when reading the system end to end. Over-decoupled systems are hard to follow, because the path from “something happened” to “here is the effect” passes through too many hops.
- Wrong abstractions are worse than mild duplication. Coupling reduction through a premature interface can create something more brittle than what it replaced — the interface ossifies the wrong cut, and now every consumer is coupled to a shape that doesn’t match the territory.
- Some coupling is intrinsic and shouldn’t be hidden. A consumer that genuinely needs to act on the failure mode of its dependency can’t be insulated from that failure mode by an interface that pretends it doesn’t exist. The vocabulary helps you tell necessary coupling from accidental coupling; both exist.
- Bounded coupling can still concentrate. Even a well-decoupled system can have a single load-bearing module that everything depends on. The coupling count looks low everywhere except at that node, and the node becomes the bottleneck for every refactor that touches it.
The goal isn’t zero coupling; the goal is appropriate coupling at the right level of the hierarchy, with the inevitable kind named and the accidental kind removed.
Related Patterns
Sources
- Wayne Stevens, Glenford Myers, and Larry Constantine introduced coupling and cohesion as named measures in “Structured Design” (IBM Systems Journal, 1974), the paper that launched structured design and established the coupling hierarchy used here.
- Larry Constantine and Ed Yourdon’s Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design (1979) is the canonical book-length treatment and the source most later textbooks draw from.
- David Parnas’s “On the Criteria To Be Used in Decomposing Systems into Modules” (Communications of the ACM, 1972) framed the underlying principle: modules should hide design decisions so that coupling is confined to stable interfaces rather than volatile internals.
- The blast-radius framing for change impact comes from the DevOps and SRE community, where it is common vocabulary for reasoning about how a single change can propagate through a tightly coupled system.