Every system design conversation eventually arrives at the same fork: connect these two things directly, right now, and ship faster — or draw a boundary between them and pay for it later in extra plumbing. The direct connection almost always wins the room. It's faster this sprint, it's fewer files touched, and nobody has to write an interface for something that "will obviously never change."
Then it changes.
Why coupling feels productive
Coupling is what happens when two parts of a system reach into each other's internals instead of talking through a defined seam. It's seductive because it's fast — one extra database join, one shared struct, one direct function call across a module boundary, and the feature ships today. Every individual coupling decision looks small. The debt shows up later, all at once, the day two teams need to deploy those "obviously never going to change" pieces independently and discover they can't.
Four shapes coupling takes
Shared tables
Two services reading and writing the same database table. Neither can change its schema without coordinating a migration across both.
Deploy ordering
Service B breaks unless Service A is deployed first. The dependency is invisible until a release goes out of order.
Shared assumptions
Two teams both assume a field means the same thing, with nothing enforcing it. The drift is silent until it isn't.
Shared release trains
Independent services bundled into one deploy pipeline. A change to one forces every other team to ship on its schedule.
The migration that a boundary made boring
The clearest case for a boundary shows up in hindsight, during a migration. A payments service that talked to the rest of the platform only through a defined event contract — never a shared table, never a direct internal call — turned what should have been a six-team, multi-week coordination effort into a change made by one team, on one schedule, that nobody else even had to notice. The boundary had been drawn a year earlier, for reasons that felt almost unnecessary at the time. It stopped being unnecessary the moment the system needed to change.
A boundary is a bet that this part of the system will need to change on its own schedule someday. You rarely lose that bet — you just don't know yet which boundary was the one that mattered.
Where boundaries go too far
Over-decoupling is a real failure mode, not a hypothetical one. Splitting two things that always change together into separate services just because "boundaries are good practice" adds network calls, retry logic, and failure modes to a relationship that was never actually independent. The cost of a boundary is real — it's paid in latency, in operational surface area, in one more thing that can time out. A boundary only earns its cost when the two sides genuinely have separate reasons to change.
The practical test isn't "could this be a boundary" — almost anything could. It's "do these two pieces have different owners, different release cadences, or different reasons to exist." If the honest answer is no, the shortcut isn't technical debt. It's just correctly modeling that these things are, in fact, one thing.
The takeaway
Coupling isn't a mistake you make once and fix later — it's a decision made dozens of times a week, mostly by default, mostly under deadline pressure. The architects whose systems age well aren't the ones who avoided every shortcut. They're the ones who could tell, in the moment, which shortcut they could afford and which one they'd be explaining in a postmortem eighteen months from now.