DEV Community

Odinaka Robert Nnamani
Odinaka Robert Nnamani

Posted on

Your microservices aren’t decoupled

Splitting a monolith into services gives you the vocabulary for free: independent, decoupled, autonomous. The architecture has to earn those words — and most don’t.

There’s a fast way to tell whether yours did. Don’t look at the deployment diagram. Look at where the contracts live.

The setup almost everyone starts with

In a gRPC system, the contracts are the .proto files: the message shapes and service definitions every service agrees to speak. They have to live somewhere, and somewhere usually means "inside whichever service was built first."

So the orders service owns the protos. The users service needs them, so it imports them straight from the orders repo. Payments needs the same messages, and rather than set up the import, someone copy-pastes the files and moves on.

orders-service/          ← .proto files live here
├── users-service        imports them from orders
└── payments-service     copy-pasted them
Enter fullscreen mode Exit fullscreen mode

It works. It ships. And it quietly wires every service to every other service.

The hidden coupling

Coordinated deployments. The moment a contract lives inside one service, changing it stops being one team’s change. Rename a field, add a required field, and every service importing that proto has to move with you. Your "independent" services now deploy in lockstep — the exact thing microservices were supposed to end.

Breaking change chaos. A breaking change in the proto doesn’t break one build. It breaks every consumer, including the ones you forgot existed. The blast radius of a contract change becomes the whole system.

Version mismatch bugs. The copy-paste in payments drifts. It always drifts. Now two services disagree about the contract while both believe they’re current — and the mismatch doesn’t show up in CI, because each service compiles fine against its own copy. It shows up in production, as garbled payloads and errors nobody can reproduce locally.

Tight inter-service dependency. Underneath all of it: services are depending on each other’s code. Not on an interface — on an implementation, owned by another team, on another team’s release schedule.

This is not independence. It’s a monolith that pays network latency for its coupling.

The fix: contracts are a product, not a folder

Contracts get their own repo.

users-service    ──┐
orders-service   ──┼──►  contracts repo (versioned, published)
payments-service ──┘
Enter fullscreen mode Exit fullscreen mode

Version it. The contracts repo releases like a library: v1.4.2, with breaking changes forcing a new major version instead of ambushing consumers.

Publish it. Each service pulls the contracts as a versioned package — the same way it pulls any other dependency.

Consume it. Every service pins the version it was built against and upgrades deliberately, on its own schedule, after its own tests pass. A team that needs the new field upgrades this sprint; a team that doesn’t stays on the old version until it’s ready.

And the hard rule that makes it real: no service imports another service’s code. Not the protos, not a shared "utils" package that quietly becomes a coupling channel. Services meet at the contract, nowhere else.

Why it works

  • API-first architecture. The contract becomes the designed artifact — reviewed, versioned, and intentional — instead of a byproduct of whichever service happened to be written first.
  • Independent deployments. A contract change ships as a new version, not a coordinated release. Teams deploy when they’re ready.
  • Controlled breaking changes. Breaking changes still happen; now they’re explicit, versioned, and adopted consumer by consumer instead of detonating everywhere at once.
  • Strong service boundaries. When the only shared thing is a published contract, the boundary between services is real — enforced by the dependency graph, not by convention and good intentions.

The principle

Services depend on abstractions. Not implementations.

It’s an old rule, and gRPC doesn’t exempt you from it — it just changes what the abstraction is. In a gRPC system, your .proto files are the abstraction. Everything else follows from where you put them.

Run the audit

Three questions, answerable in five minutes:

  1. Does one service "own" your proto files?
  2. Do services import code from each other?
  3. Are your contracts versioned independently of any single service?

Three yeses to the wrong questions, and the diagnosis writes itself: the services are coupled at the contract layer, and no amount of separate repos, pipelines, or deploy targets changes that.

Decoupling was never a property of how many services you run. It’s a property of what they’re allowed to depend on.

If your contracts aren’t independent, your services aren’t either.


I build email infrastructure (rootmail, in beta) and write about backend systems every week. More here: https://robertpaschal.substack.com

Top comments (1)

Collapse
 
abhinavs1920 profile image
Abhinav Sharma •

The where-do-the-contracts-live test is a sharp one. Protos living inside the first built service means every consumer silently depends on that team's release cycle, which is coupling wearing a decoupled costume.