Your Microservices Are Too Dependent on Each Other
by Eric Hanson, Backend Developer at Clean Systems Consulting
The test your architecture is probably failing
Can you deploy any single service in your system without coordinating with the team that owns any other service? If the answer to this question involves exceptions — "yes, except for Service A, which needs a new field from Service B" or "yes, but we have to deploy them in a specific order" — your services are too coupled. The entire promise of microservices — team autonomy, independent deployment, isolated failure domains — depends on genuine independence. Partial independence just means you have a distributed system with deployment coordination requirements on top.
Coupling in microservices is not always obvious. The most common forms don't look like coupling until they cause an incident or a deployment failure.
Shared database coupling
The most severe and most common: multiple services read from or write to the same database tables. This creates hidden coupling at the schema level — any schema change affects every service touching those tables, and those services must be deployed together or in a carefully coordinated sequence.
The tell is a shared database user with write access across schema namespaces, or a single database with tables owned by multiple services. The fix is data isolation: each service owns its schema, and other services access that data only through the owning service's API. If another service needs frequent access to data it doesn't own, consider event-driven replication of a read model into the consuming service's own storage.
Platform library coupling
Shared libraries across services are fine for infrastructure concerns: logging utilities, HTTP client wrappers, authentication helpers. They become coupling when they contain domain logic — entities, business rules, event definitions.
If your common-domain library defines the Order class and six services import it, you've created a deployment dependency. When Order needs a new field, you update the library, republish it, and every service must adopt the new version. If Service A adopts v2 of the library but Service B is still on v1, and they share event schemas defined in the library, they can't communicate correctly.
Domain types should live in the service that owns them. Other services should have their own internal representation of shared concepts, translated at the boundary:
// Inventory Service's internal representation of an order item
// NOT imported from a shared library
public record OrderItem(
String itemId,
int quantity,
String sku
) {}
// Translation from Order Service's event payload
public OrderItem fromEvent(OrderItemPayload payload) {
return new OrderItem(payload.itemId(), payload.qty(), payload.sku());
}
This looks like duplication. It is duplication, and it's correct duplication. The Inventory Service's concept of an order item is exactly what Inventory needs — no more, no less. When Order Service changes its model, Inventory Service's translation layer is updated independently.
Synchronous call chain coupling
When Service A calls B, which calls C, which calls D, you have a call chain where A's behavior depends on B, C, and D all being available and performing adequately. This is temporal coupling — availability coupling across time.
Map your synchronous call chains. If any chain is longer than two hops (A → B → C), audit whether the intermediate calls are genuinely necessary. Common findings:
- B calls C just to pass data through — B should own or replicate that data locally
- The chain exists because the domain model is wrong — B shouldn't be calling C's domain at all
- Some calls in the chain are fire-and-forget and should be converted to async events
For chains that are genuinely necessary, isolate each hop with a circuit breaker and ensure each service has a fallback. But the long-term answer is reducing the chain depth through better domain modeling.
Deployment order coupling
If your deployment runbook says "deploy Service A before Service B because B expects A's new API to be available," you have coupling. This is usually caused by one of:
- Service B consuming a new field from Service A before Service A supports it (versioning failure)
- Service A and B sharing a database migration that must run in sequence (shared schema)
- A behavioral dependency where B's initialization calls A's API
Eliminate deployment order requirements by designing each service to tolerate the absence or older version of services it depends on. New fields should be optional with documented defaults. Schema migrations should be backward compatible for at least one deployment cycle. Initialization logic should not have hard dependencies on other services being current.
The practical audit
Take your most critical service. Enumerate every external dependency it has: databases, other services, shared libraries, message brokers. For each dependency, answer:
- Can this service deploy without this dependency being deployed first?
- Can this service function if this dependency is unavailable at runtime?
- Does this dependency's schema or API contract control when this service can deploy?
Any "no" answer identifies a coupling that reduces your deployment independence. Prioritize eliminating the database and library coupling — those are the highest-impact forms. The synchronous runtime coupling you handle with circuit breakers and fallbacks, but the design coupling you have to address at the architectural level.