skip to content

Is ADP an absolute rule? Discuss when breaking a component cycle costs more than it saves, and how cycles manifest — and are broken — at service granularity in a distributed system.

level: principalimportance: nice to knowfreq 22%

answer

  1. Cycle = evidence two components are really one
  2. In-process cost: indirection, extra component, or lost releasability
  3. Service cycles: cascading failure, deadlock, deploy lockstep
  4. Break with events, extracted contract, moved data, or merge
  5. ADP necessary not sufficient — SDP/SAP judge direction

basics

~20 s

ADP is a strong default, not a law. Breaking a cycle costs indirection, extra components, or a merge — sometimes not worth it for two tiny modules that always ship together. But at service granularity cycles are far more dangerous: they force lockstep deploys and risk cascading failure, so there the rule is close to absolute.

solid answer

~60 s

Cycles are a symptom: the graph says two components are really one. The right fix depends on scale. **In-process**, breaking a cycle costs an indirection (DIP), an extra component in the graph, or lost independent releasability (merge). If both parts are small, owned by one team and always released together, merging — or accepting the cycle inside a single deployable — is a defensible trade; the classic argument for ADP (release isolation, morning-after syndrome) only bites when teams release independently. **Across services**, the calculus flips. A synchronous request cycle (A calls B calls A) adds latency multiplication, thread/connection exhaustion, retry storms, cascading failure and possible distributed deadlock, plus a deploy-order impossibility for breaking changes. It also usually indicates a wrong aggregate boundary. Break it by inverting the edge into an event (publisher no longer knows the consumer), extracting a shared contract/schema component, moving the data so one side is authoritative, or merging the two services if they truly share a transactional boundary. Watch too for *contract*-level cycles (shared client libraries) and for pushing coupling into a kitchen-sink shared module — that hides the loop rather than removing it.

go deeper

for a junior

Say ADP is a strong default; cycles between deployable services are especially bad because neither can be released or deployed alone.

for a middle

Contrast the concrete costs of DIP inversion, extraction and merging, and name the distributed hazards (lockstep deploys, cascading failure).

for a senior

Argue trade-offs by scale and ownership, describe event-based inversion with its eventual-consistency and schema-versioning costs, and warn about shared-library cycles reappearing at build time.

for a principal

Read the cycle as evidence of a wrong boundary; connect acyclicity to team autonomy and deployment topology; position ADP as the gate with SDP/SAP as the direction judgement, and define when a documented, baselined cycle is the rational choice.

## First: what ADP is really telling you A cycle is evidence, not merely a rule violation. It says: *these components change together, cannot be released apart, and cannot be reasoned about separately — so they are one unit.* The engineering choice is which truth to make explicit: split them properly, or admit they are one. ## The honest cost of breaking a cycle 1. **DIP inversion** costs an interface, a runtime indirection, and a composition root that must know the concrete type. It can also scatter a single cohesive idea across two components, hurting readability. Overused, you get "interface for everything" with one implementation each — abstraction without benefit. 2. **Extraction** costs a new component: another build target, version, owner, changelog and release. Worth it when the extracted piece is cohesive; harmful when it becomes a `common`/`shared` dumping ground that everything depends on and that changes for every reason (violating CCP — classes that change together belong together — and CRP — don't force consumers to depend on what they don't use). 3. **Merging** costs independent releasability but buys honesty and a simpler graph. ## When tolerating a cycle is defensible - Both components live in **one deployable, owned by one team**, and always ship together — the release-isolation benefit is zero, so only build/test hygiene remains as the argument. Many toolchains fuse them anyway. - The cycle is **test-only** (a module's tests use another module's helpers) and absent from shipped artifacts. Still worth cleaning, but not urgent. - **Bootstrapping**: a compiler compiled by itself, or a framework that depends on a version of itself. Broken by time — build with the previously released binary — rather than by structure. - **Deliberate, documented, and gated**: a baselined legacy cycle with an owner and a plan beats a rushed refactor that breaks behaviour. What is not defensible is an *undetected* cycle or a suppression list that only grows. Counterweight: in-process cycles compound silently. Build times grow, tests lose isolation, module boundaries become fiction, and future extraction (e.g. to a service) becomes prohibitively expensive. Tolerate only with a clear reason, not by default. ## Service granularity: the same principle, higher stakes At service scale, edges mean network calls, shared schemas and deployment coupling. A cycle brings failure modes an in-process cycle never had: - **Latency multiplication and resource exhaustion**: A → B → A occupies two request threads/connections for one logical operation; under load this exhausts pools on both sides. - **Cascading failure and retry storms**: a slowdown in either service feeds back into itself; retries amplify. Circuit breakers, timeouts and bulkheads mitigate but do not remove the structural loop. - **Distributed deadlock**: mutual synchronous calls holding locks or limited pools can wedge permanently. - **Deploy-order impossibility**: a breaking contract change requires each side to be updated first. The escape hatch is expand/contract (add the new field, migrate consumers, then remove the old) — feasible, but a permanent tax on every change. - **Autonomy loss**: two teams cannot release or scale independently, which defeats the reason for splitting the services at all. ## Breaking service cycles 1. **Invert with events.** Replace B's synchronous call back into A with an event A subscribes to. The publisher stops knowing its consumers; the runtime flow persists, the dependency arrow does not. Costs: eventual consistency, delivery semantics (at-least-once, idempotency), and event-schema versioning — the coupling moves to the schema rather than vanishing. 3. **Extract a shared contract or a third service.** Pull out the shared concept both sides need (identity, pricing, catalogue) so both depend on it and neither on the other. 4. **Move the data / fix the boundary.** A cycle very often means the aggregate boundary is wrong — the data one service keeps asking the other for should live with the asker, or the two should be one. 5. **Merge.** If the pair shares a transactional invariant, they were never two services; merging removes a distributed cycle *and* a consistency problem. Beware pseudo-fixes: a shared client library that both services depend on can re-create the cycle at build time even after the runtime call is removed; and an intermediary added purely to "not be a cycle" while both sides still block on each other retains every failure mode. ## Where ADP sits Acyclicity is necessary, not sufficient. **SDP** (Stable Dependencies Principle) says depend in the direction of stability — components that are hard to change should not depend on volatile ones. **SAP** (Stable Abstractions Principle) says stable components should be abstract, so they can be extended without modification. A legal DAG with arrows pointing from stable core into a volatile UI is still bad architecture. Use ADP as the hard gate and SDP/SAP as the direction-of-travel judgement.

  • Two microservices call each other synchronously. What single change most cheaply removes the structural cycle?
    Invert one direction into an event: the service that was being called back publishes an event the other subscribes to, so the publisher no longer knows its consumer. Accept eventual consistency, idempotent handling and event-schema versioning as the price.
  • After moving to events, both services still depend on a shared client library. Is the cycle gone?
    Not necessarily. If both depend on the shared library and it depends back on either, the loop returns at build time. Shared contracts must sit in a leaf component that depends on neither side.
  • How do ADP, SDP and SAP relate?
    ADP is the hard gate — no cycles. SDP says arrows should point toward more stable components, and SAP says stable components should be abstract. A DAG can still be badly directed; you need all three to judge the graph.

saying these in an interview costs you the question

  • Treating ADP as absolute at every granularity without weighing the cost of the fix.
  • Claiming distributed service cycles are equivalent to in-process cycles — they add latency multiplication, cascading failure and deadlock risk.
  • Assuming introducing events removes coupling entirely; it relocates it to the event schema and adds eventual-consistency burden.
  • Adding a shared 'common' library or a pass-through intermediary and declaring the cycle solved while both sides still block on each other.
  • Believing an acyclic graph is automatically a good architecture — SDP/SAP still govern direction and abstractness.
  • Merging two services purely to satisfy a diagram, without checking whether they share a transactional invariant.

context