Loose coupling is one of those things everyone agrees with until they have to do it. Then it turns into a mess of interfaces, event buses, and six layers of indirection for a feature that just sends an email.

I've built both kinds of systems: tightly coupled ones that were fast to write and painful to change, and over-abstracted ones that were painful to write and still painful to change. Here's what I've settled on.

Coupling isn't binary

The question isn't "are these services coupled?" It's "what changes together, and how often?"

Two services are tightly coupled if a change in one forces a coordinated change in the other. If they share a database schema, a deploy, or a data format, they're coupled. The goal isn't zero coupling. It's coupling that matches the rate of change. Things that change together should be coupled together.