Coupling Is the Only Thing That Matters
SRP, DRY, the Law of Demeter, dependency inversion, bounded contexts, messaging — a dozen rules you already know are one idea wearing different hats. Here is the single question underneath them, why decoupling can never actually remove coupling, and how to tell which kind you are fixing.
SRP. DRY. The Law of Demeter. Dependency inversion. Open-closed. Interface segregation. Acyclic dependencies. Bounded contexts. Ports and adapters. Publish-subscribe over direct calls.
A junior engineer memorises that list as ten rules. A staff engineer sees one rule, stated ten times, at ten different altitudes.
The rule is: decide what a change here forces to change over there. Everything else on that list is a tactic for shaping the answer at some particular scale. If you learn the force instead of the tactics, you stop asking "which principle applies here?" and start asking one question that works from a single function up to a fleet of services.
The claim, minimally stated
Coupling has been given rich, careful, chapter-length definitions many times over. None of them survive contact with a design review. The one that does is embarrassingly thin:
Two parts of a system are coupled if a change in one might force a change in the other.
That thinness is the point, not a shortcoming. Coupling is a claim about change propagation and nothing else. Not about layers, not about elegance, not about how many interfaces you have. Just: if I edit this, what else moves?
Which leads immediately to the part most people skip. You cannot remove coupling. Every system has essential coupling — a checkout flow genuinely does depend on pricing, and pretending otherwise produces fiction, not architecture. The goal is never zero. The goal is to make the coupling you have intentional, minimal, and visible.
The same idea, six times
Here is one running example — an order total — climbing the ladder. Watch the tactic change while the question stays identical.
Function scale. A function that does one thing couples its callers to one thing.
// Two responsibilities, so two reasons to change, so callers move when either does.
function totalOrder(order: Order, applyTax: boolean): number { … }
// One thing. Tax is a separate decision with a separate reason to change.
function subtotal(order: Order): number { … }
function withTax(amount: number, rate: TaxRate): number { … }
That boolean parameter is the tell — a flag argument means the function does one thing when true and another when false. SRP is usually taught as "one reason to change," which is vague enough to be unusable. The sharper version: a reason to change is an actor. A module should answer to one actor, because when finance and fulfilment both own the same class, a change requested by one breaks the other.
Knowledge scale. DRY is not "don't paste code." The rule is that every piece of knowledge must have a single, unambiguous, authoritative representation. Two functions that happen to look alike are not a violation. Two places that both know the tax rounding rule are — because a change to that rule now has two edit sites, and the question was never whether you would remember the second one. It is when you will forget.
Object-interaction scale. The Law of Demeter forbids reaching through one object to reach another:
// A train wreck: hard-codes the shape of two objects you never meant to depend on.
const city = order.getCustomer().getAddress().getCity();
// Tell, don't ask.
const rate = order.taxRateFor(taxTable);
The first line depends on Customer having an Address having a City. Reshape any of those and this breaks — even though it only ever wanted a tax rate.
Dependency-direction scale. Dependency inversion does not remove the dependency between policy and detail; it reverses which one is fragile. An architectural boundary is a line across which dependencies are controlled, separating policy that changes for business reasons from detail that changes for technical reasons. Pricing rules should not move because you swapped Postgres for DynamoDB.
Component scale. Acyclic dependencies and stable-dependency direction are the same instinct counted rather than felt, and at this scale it really is countable: afferent coupling (fan-in — how many components point at this one), efferent coupling (fan-out — how many it points at), total being the sum.
System scale. The four integration styles — File Transfer, Shared Database, RPC, Messaging — are four coupling decisions wearing an integration costume. Shared Database's hidden cost is coupling through the schema: one migration breaks every consumer. RPC's hidden cost is treating a remote call like a local one. Messaging buys decoupling in three dimensions — space, time, and format — and pays in eventual consistency. Bounded contexts are the same move applied to meaning: inside the boundary a word has one definition, and integration happens across an explicit contract instead of a shared table.
Six scales. Six vocabularies. One question.
The part that changes how you argue about it
Here is the finding that should make you suspicious of your own refactors.
Take a decoupling refactor you are proud of. Two components had a direct dependency; now they do not, and each can change without disturbing the other. Then stop looking at the pair and look at the whole system.
The knowledge did not evaporate. Something still has to know that those two participate in the same workflow — a coordinator, an event contract, a subscription, a broker topology. You did not delete a dependency. You moved it out of the pair and into the space around them, where it no longer shows up in either component's imports and no longer shows up in your count.
Decoupling does not delete knowledge. It relocates it, and you chose the destination.
Decoupling has a conservation law. The workflow still has to be known by someone; you only chose who. And that choice has a real shape:
| Tightly coupled | Loosely coupled | |
|---|---|---|
| Workflow knowledge | Centralised — one component knows the flow | Distributed — spread across components |
| To understand the workflow | Read one place | Visit several |
| Risk per change | Higher — changes ripple | Lower — changes stay local |
That table reappears one scale up as orchestration versus choreography, and again at deployment scale in service granularity. It is also the strongest argument against reflexive "just decouple it" advice. You are not removing knowledge. You are deciding where it lives, and paying to look it up there forever after.
Which kind are you actually fixing?
The other distinction worth internalising, which goes back to Meilir Page-Jones, is static versus dynamic coupling:
- Static is how parts are wired: what must exist for this thing to run at all. Operational. Visible at build and deploy time.
- Dynamic is how parts call each other during a workflow. Communication. Visible only at runtime.
They have different fixes, and conflating them produces unwinnable design arguments. Two services sharing a database are statically coupled no matter how cleanly they message each other. Two services with private databases calling each other synchronously are dynamically coupled no matter how independent their deployment pipelines look.
Which produces the most common expensive mistake in this whole area: a team introduces a message broker and goes asynchronous — attacking dynamic coupling — while the real problem is the shared schema underneath, which is static. The architecture diagram gets prettier. Nothing becomes more independently deployable. Months, spent.
Before you decouple anything, name which kind you are fixing. If you cannot, you are not ready to spend the effort.
The dosage
None of this argues for maximum decoupling. Push the dial all the way and nothing in the system can talk to anything else; that is not an architecture, it is a set of unrelated programs sharing a repository. Coupling is not inherently bad. It is the thing that makes a collection of parts into a system.
Paracelsus got there first, about a different subject: the dose makes the poison.
"Loosely coupled" is not a target. It is a dial with costs at both ends. Turn it too far and you get indirection nobody can trace, interfaces with one implementation, DTOs that exist to convert other DTOs, and eventual consistency in a system that was never distributed to begin with. Hexagonal ceremony wrapped around CRUD is not architecture; it is cost with no matching benefit.
The first law of software architecture is that everything is a trade-off — with the corollary that makes it usable: if you find a decision that doesn't have one, you haven't looked hard enough. Spend decoupling where change is both expensive and likely. Everywhere else, coupling is not debt. It is just the shortest path — and quality is a requirements decision, this one included.
The one question
You do not need to remember ten principles. You need to ask, at whatever altitude you happen to be standing:
What does this couple to, and can the two change independently?
Then three follow-ups that turn the answer into a decision:
- Is this coupling static or dynamic? Operational wiring and runtime conversation have different fixes.
- If I decouple it, where does the knowledge go? It does not evaporate. Name its new home and accept the lookup cost.
- Is this change expensive and likely? If not, leave it coupled and spend the complexity budget somewhere it pays.
Every rule on that opening list is a cached answer to question one, computed at some particular scale by someone who had already internalised the force. Learn the force. The rules become corollaries you can re-derive — and, more usefully, know when to break.