Microservices

Breaking Service Dependency Cycles

Service dependency cycles make microservices impossible to deploy, test, or reason about in isolation. How to detect them and four ways to break them.

Part of Microservice Service Design: Boundaries That Hold
Breaking service dependency cycles, shown as nodes in a closed loop with one amber link being cut

Service dependency cycles are one of the clearest signs that a microservice architecture has gone wrong. When A calls B, B calls C, and C calls back to A, you no longer have independent services, you have one distributed unit that happens to run in separate processes. The whole benefit of splitting services, deploying and reasoning about them independently, is gone, and the cure is to break the cycle deliberately rather than route around it.

A cycle is insidious because each individual dependency looked reasonable when it was added. Nobody sets out to build a loop; it accretes one sensible-seeming call at a time until the day you cannot deploy one service without the other two.

Why dependency cycles defeat the point of microservices

The reason to split a system into services is independence: each can be deployed, scaled, tested, and owned on its own. A dependency cycle removes exactly that property. If A, B, and C form a loop, none of them is independent of the others, so you have paid the full operational cost of three services and kept the coupling of a monolith.

This is the distributed monolith failure in its purest form, and it is why dependency direction is an architectural concern, not an implementation detail. This post is part of the Service design series.

Why are circular dependencies between services bad?

Circular dependencies couple services that are supposed to be independent, and the damage shows up across the whole lifecycle. You cannot deploy one service in the cycle without the others being compatible, startup ordering becomes ambiguous (each waits on another), a change in one can ripple around the ring, and a failure in any member can cascade to all of them.

The concrete symptoms:

  • Lockstep deploys. You cannot release A without coordinating B and C.
  • Ambiguous startup. Each service depends on another being up, so cold-start ordering is undefined or deadlocks.
  • Cascading failure. A slowdown in C backs up B, which backs up A, which (via the cycle) worsens C.
  • Untestable in isolation. You cannot stand up one service for a test without standing up the loop.
  • Reasoning breaks down. “What depends on what” has no answer, because everything depends on everything.

Each symptom traces back to the same root: a graph that should be acyclic has a loop in it.

How do you detect service dependency cycles?

Build a directed graph of which service calls which, then run cycle detection on that graph. You can construct the graph from static analysis of clients, from your service mesh’s observed traffic, or from a dependency manifest. The high-value move is to turn this into a CI check that fails the build when a new dependency would introduce a cycle, so the graph stays acyclic by policy instead of degrading silently.

Service mesh telemetry makes detection easier in a running system: the mesh already knows the real call graph, so you can periodically extract it and check for cycles you did not design. Pair that observed graph with the CI guard, and you catch both the cycles already present and the ones someone is about to add.

How do you break a circular dependency between services?

There are four standard fixes, and the right one depends on why the cycle exists. The most common production fix is to invert the back-edge with an event, so the service that closed the loop no longer makes a synchronous call.

FixWhen to use itTradeoff
Invert with eventsC needs to notify A, not query itAsync; eventual consistency
Extract a shared serviceA and C both need shared logic/dataOne more service to own
Merge the servicesA and C are really one bounded contextLarger service, less independence
Introduce an interfaceThe dependency can point one wayIndirection; still synchronous

Invert with events. If the back-edge exists because C needs to tell A something happened, replace the synchronous C → A call with C emitting an event that A consumes. The call graph becomes acyclic because C no longer depends on A; it just publishes. This is the workhorse fix, and it connects directly to Kafka replay and idempotent consumers.

Extract a shared service. If A and C both depend on each other because they share some logic or data, pull that shared concern into a new service D that both depend on. The cycle A ↔ C becomes A → D ← C, which is acyclic.

Merge the services. Sometimes the cycle is telling you the truth: A and C are not actually two services, they are one bounded context that was split incorrectly. Merging them removes the cycle and is the right call when they always change together.

Introduce an interface. If the dependency genuinely needs to be synchronous but only points one way conceptually, an interface or dependency inversion can make the concrete dependency point the acyclic direction. This is the least common fix at the service level and the most common within a single service’s code.

Are dependency cycles ever acceptable?

Almost never between services, and only narrowly within a single service’s code. At the service boundary a cycle reliably means lost independence, so it should be treated as a defect to remove, not a tradeoff to accept. The rare exception is a tightly-scoped, in-process cycle inside one service where the components genuinely co-evolve and ship together anyway.

The reason the bar is so high at the service level is that the cost of a cycle there is structural: it removes the ability to deploy, scale, and reason independently, which is the entire justification for having separate services. There is no clever pattern that makes a cross-service cycle safe; the patterns all aim to remove it. If you find yourself arguing that a particular service cycle is fine, that is usually a sign the two services should be one.

Within a single service, small cycles between closely-related modules are sometimes pragmatic, because everything in that service deploys together regardless, so the independence argument does not apply. Even there, an acyclic design is usually cleaner and easier to test, but it is a code-quality preference rather than the architectural hazard a cross-service cycle represents. Keep the hard line at the service boundary, where it matters most.

A dependency-cycle playbook

When you find a cycle, work through it in this order:

  • Confirm the cycle from the real call graph (mesh telemetry or static analysis), not from memory.
  • Identify the back-edge, the dependency that closes the loop.
  • Ask whether that back-edge is a notification (then use events) or a query (then extract or invert).
  • If the two services always change and deploy together, consider that they should be merged.
  • Add a CI acyclicity check so the cycle cannot reappear once removed.
  • Re-check the graph after the fix to confirm it is now acyclic.

What I’d do differently

The mistake that creates cycles is treating each new inter-service call as a local decision. It is not; every call adds an edge to a global graph, and the graph’s shape is an architectural property. By the time a cycle is painful, it has usually been there for months, hidden behind individually-reasonable calls.

If I were running a microservice system from the start, I would make the service dependency graph a first-class, monitored artifact with an acyclicity check in CI, the same way teams guard against Protobuf schema breakage. Keeping the graph acyclic by policy is far cheaper than untangling a loop after it has fused three services into one. The direction of your dependencies is something you should decide on purpose, not discover during an incident.

Sources

Frequently asked questions

What is a service dependency cycle?

A service dependency cycle is when service A depends on B, B depends on C, and C depends back on A, forming a loop. The services can no longer be deployed, tested, or reasoned about independently, which defeats the main reason for splitting them apart in the first place.

Why are circular dependencies between services bad?

They couple services that should be independent. A cycle means you cannot deploy one service without the others, a change in one can break the loop, startup ordering becomes ambiguous, and a failure in any member can cascade around the ring. The services behave like one tangled unit.

How do you detect service dependency cycles?

Build a dependency graph from your service call map or service mesh telemetry and run cycle detection on it. Many systems add a CI check that fails the build if a new dependency introduces a cycle, so the graph stays acyclic over time rather than degrading silently.

How do you break a circular dependency between services?

Common fixes are inverting the dependency with events so the caller no longer calls back synchronously, extracting the shared concern into a third service both depend on, merging two services that are truly one, or introducing an interface so the dependency points one way. Events are the most common production fix.

Newsletter

Liked this breakdown?

Production wisdom on distributed systems, delivered when there is something worth saying. No spam, unsubscribe anytime.