This is part two of a series on microservices architecture. If you’re not sure whether microservices are the right call for your team yet, start with the decision checklist before reading further.
Say you’re building the backend for an online CD retailer. We’ll call it MusicCorp, and we’ll keep coming back to it in this series. Orders need payment taken, stock reserved, packages shipped, loyalty points awarded. Somebody on the team draws a box around each of those verbs, calls it a microservice, and ships it. Six months later, changing how loyalty points work requires touching four different services in lockstep. What went wrong?
Almost always, it’s the boundary, not the technology. Kubernetes, gRPC, and a service mesh won’t save you from a bad cut. This post is about the three ideas that actually predict whether a boundary will hold up: information hiding, cohesion, and coupling, plus the specific kinds of coupling worth telling apart, because “avoid coupling” is useless advice until you know which coupling you’re looking at.
Information hiding: the one rule underneath everything else
David Parnas, writing about module design decades before “microservice” was a word, put it simply:
The connections between modules are the assumptions which the modules make about each other.
Every assumption one service makes about another’s internals is a thread that will eventually snap when someone changes those internals. The fewer assumptions, the more freely each side can change. A microservice’s whole value proposition (deploy this one thing without deploying anything else) depends on ruthlessly hiding as much as possible behind its interface: internal data structures, database schema, business logic, all of it. Expose only the minimum needed to satisfy consumers.
Cohesion: the code that changes together, stays together
Cohesion asks a different question than coupling: it’s about what’s inside your boundary, not what crosses it. The pithiest definition: the code that changes together should live together. If a single business change (say, “loyalty points now expire after 12 months”) requires edits spread across three services, that’s weak cohesion; the related behavior never had a coherent home. Strong cohesion means one change, one deploy.
Coupling and cohesion aren’t independent: they’re two views of the same underlying question, just measured from inside vs. outside the boundary. Larry Constantine’s law says it cleanly: a structure is stable if cohesion is strong and coupling is low. Neither one alone is sufficient.
Coupling comes in flavors, and they are not equally bad
This is the part most teams skip, and it’s the part that actually matters. “Loose coupling good, tight coupling bad” doesn’t tell you what to do when you’re staring at a design and trying to decide if it’s fine. Here are four kinds of coupling you’ll run into between microservices, ordered from least to most dangerous.
Domain coupling: usually fine, in moderation
Domain coupling is simply one service calling another because it needs that
other service’s functionality. OrderProcessor calls Warehouse to reserve
stock and Payment to take money. This is largely unavoidable (a system
made of collaborating services has to collaborate) and it’s considered the
loosest, most acceptable form of coupling.
The warning sign isn’t the coupling itself, it’s the shape of it: if one service depends on a long list of downstream services, that’s often a symptom that too much logic and responsibility has piled up in the caller. Keep the fan-out small, and keep what you send across the boundary to the minimum the callee actually needs; information hiding again.
Pass-through coupling: the sneaky one
This happens when a service passes data through to a second caller purely
because a third, further-downstream service needs it. Picture
OrderProcessor sending a ShippingManifest to Warehouse, which does
nothing with it except forward it to Shipping. Now OrderProcessor has to
know about a data shape that belongs, conceptually, to a service two hops
away. Change what Shipping needs, and the change potentially ripples all
the way back to OrderProcessor; three services now need a coordinated
release for what should have been an internal detail of one.
The fix is usually one of:
- Bypass the intermediary: have the caller talk to the real owner directly. Trades pass-through coupling for a bit more domain coupling, which is a good trade, but only if it doesn’t push logic that belonged to the intermediary back up into the caller.
- Let the intermediary own the shape: have
Warehousecollect what it needs and construct theShippingManifestitself, soShipping’s contract changes become invisible toOrderProcessor. - Treat the payload as an opaque blob:
OrderProcessorstill sends the manifest throughWarehouse, butWarehousenever looks inside it, just relays it. This doesn’t eliminate the coupling between the two endpoints, but it does mean the middle service never needs to change when the shape does.
Common coupling: fine for read-only reference data, risky otherwise
Common coupling is what you get when two or more services read and write the same shared data (classically, a shared database table). Multiple services reading static, rarely-changing reference data (country codes, tax rates) from one store is relatively benign, because that data barely changes and nobody’s fighting over write access.
It gets dangerous the moment multiple services both write to the same
structure. Imagine OrderProcessor and Warehouse both updating a Status
column on the same Order row: one setting PLACED/PAID/COMPLETED,
the other setting PICKING/SHIPPED. Nothing stops an invalid transition
like PLACED → SHIPPED from slipping through, because neither service has
a complete view of what’s allowed. The fix is to give the state machine a
single owner: an Order service that both callers request changes from,
and that can reject a request that violates its own rules. For the
practical patterns of how to structure data when services can’t share a
database (API composition, CQRS, event sourcing), see
database per service.
If a “service” is really just a thin wrapper over database CRUD (every request maps straight to an update, no rules applied), that’s a sign the logic that should live there has leaked out into every caller instead. You’ve traded one strongly-coupled shared table for several services all independently guessing what’s a valid state transition.
Content coupling: just don’t
Content coupling is common coupling’s uglier sibling: instead of a
known shared dependency, an outside service reaches directly into
another service’s internal storage and mutates it, bypassing the owning
service’s API entirely. Say Warehouse writes directly to the Order
table instead of calling the Order service.
The difference from common coupling is subtle but important: with common
coupling, everyone at least knows they share an external dependency they
don’t fully control. With content coupling, the lines of ownership
disappear. The Order table becomes part of an external contract nobody
agreed to, information hiding is gone, and you’re now trusting that
Warehouse’s idea of “valid state transition” exactly matches the Order
service’s, with zero way to enforce it. This is sometimes called
pathological coupling for a reason. Avoid it outright.
A quick word on temporal coupling
One more form worth knowing, because it comes up constantly once you start
picking communication styles (the subject of the next post): temporal
coupling is when two services both need to be up and reachable at the same
instant for an operation to succeed. A synchronous HTTP call from
OrderProcessor to Warehouse is temporally coupled: if Warehouse is
down, the whole operation fails right then. It’s not inherently bad, but as
call chains grow, temporal coupling compounds into cascading failures.
Asynchronous communication is one of the main tools for loosening it.
Naming things the way your users do
One more foundational habit worth adopting alongside coupling analysis:
ubiquitous language, borrowed from Domain-Driven Design. Use the same
terms in your code that domain experts use when they talk about the
business. The alternative (a generic, one-size-fits-all data model where
every real-world concept becomes a vague Arrangement or Entity) forces
constant mental translation between what the business means and what the
code says, and that translation tax gets paid by every developer, forever.
A related DDD concept, the aggregate, is worth carrying forward too: an
aggregate is a real domain concept with its own identity and lifecycle
(an Order, an Invoice), modeled as a self-contained unit whose state
transitions are managed together, in one place, by one service. An
aggregate gets to say no to an invalid request. That idea (a service
guarding its own state machine rather than letting callers dictate it
directly) is exactly what breaks the common-coupling trap above, and it’s
worth keeping in your back pocket for the rest of this series.
Boundaries are the foundation, but they don’t tell you how two well-bounded services should actually talk to each other at runtime: synchronously, asynchronously, as a request or as a broadcast event. That’s next.