skip to content

What is the Acyclic Dependencies Principle, what concrete problems do dependency cycles between components cause, and what are the two standard ways to break a cycle?

level: middleimportance: should knowfreq 52%

answer

  1. component graph must be a DAG
  2. cycle = one release unit in disguise
  3. morning-after syndrome
  4. invert an edge, or extract a shared component
  5. sometimes just move the misplaced class

basics

~20 s

The Acyclic Dependencies Principle says the component dependency graph must have no cycles. Cycles make components impossible to build, test, or release separately. You break them either by inverting one edge with an interface, or by extracting the shared part into a new component both depend on.

solid answer

~50 s

ADP: "allow no cycles in the component dependency graph" — the graph must be a DAG. A cycle means the components in it are effectively one component: you cannot compile, test, version, or release any of them without all the others, small changes force wide rebuilds, and every developer is exposed to everyone else's in-progress work (the 'morning after syndrome' ADP was coined to solve). Cycles also break the release-train model where each component publishes versioned releases its dependents adopt on their own schedule. Two canonical fixes: (1) apply DIP — invert the offending edge by declaring an interface on the side that should stay stable so the arrow reverses and the cycle opens; (2) extract a new component containing the elements both sides need, and point both at it. Which one you choose depends on whether the coupling is behavioral (invert) or shared-data/shared-abstraction (extract). Prevention beats cure: detect cycles in CI with build-graph or architecture tests, because cycles form silently one import at a time.

go deeper

for a junior

Define the principle (no cycles in the component graph) and name one consequence — you cannot build or release the components separately.

for a middle

Explain the morning-after/build-and-release consequences and demonstrate both fixes — invert an edge with an interface, or extract a shared component — with an example.

for a senior

Distinguish component cycles from class cycles, discuss detection in CI, and warn about the extracted 'common' component turning into a junk drawer.

for a principal

Treat the component graph as a maintained artifact tied to build times, release trains, and team ownership; extend the analysis to cross-service cycles and deploy-order coupling.

## Terms - **Component**: the smallest unit that can be independently released — a package/module/library/jar/assembly/npm package. Not a class. - **Component dependency graph**: nodes = components, edge A → B = A's source names something in B. - **Cycle**: a path that returns to its start, e.g. A → B → C → A. Length 2 (A ⇄ B) counts. - **DAG**: directed acyclic graph — the target shape. ## The principle > **Acyclic Dependencies Principle (ADP): allow no cycles in the component dependency graph.** ## Why cycles hurt — concretely, not abstractly 1. **They fuse components.** A cycle's members can only be built, versioned, and released as a unit. Three "components" in a cycle are one component with extra folders and a misleading diagram. 2. **Morning-after syndrome.** The original motivating story: you leave code working; overnight others change modules yours depends on — and, because of cycles, modules that depend on yours too — and nothing builds in the morning. Nobody can stabilize anything. 3. **Build/test cost.** Incremental builds and test-impact analysis rely on topological order. A cycle collapses that: a one-line change rebuilds and retests the whole cluster. 4. **No independent release.** Semantic-versioned releases assume you can pin a dependency and upgrade on your own schedule. In a cycle, A's new version needs B's new version which needs A's — a chicken-and-egg release deadlock. 5. **Class-loading/initialization hazards.** Static initialization order across a cycle is fragile and language-dependent; some toolchains simply refuse cyclic modules. 6. **Comprehension cost.** There is no "start here" — you cannot read the system in layers, and every component is potentially reachable from every other. ## Fix 1 — invert an edge (DIP) Cycle: `Billing → Notifications → Billing` (Billing sends receipts; Notifications reads an invoice formatter from Billing). Invert the *second* edge: declare in Notifications an interface `MessageBody` (or have Billing pass a plain rendered string). Billing implements/supplies it. Now `Billing → Notifications` only. Rule of thumb: the interface goes on the side that should remain the *stable* one; the other side implements it. This works best when the coupling is **behavioral** — one side needs something *done*. ## Fix 2 — extract a new component Cycle: `Orders ⇄ Customers`, both because they share the `Money`/`Address` value types and a couple of enums. Neither is really "using" the other's policy. Extract `SharedKernel`/`CommonTypes`: ``` Orders ──▶ CommonTypes ◀── Customers ``` This works best when the coupling is **shared vocabulary/data**. Danger: the extracted component becomes a junk drawer that everything depends on and that itself starts depending on everything — a cycle magnet. Keep it tiny, stable, abstraction-heavy, and dependency-free, and treat any new import inside it as a design review. A third, less-cited option: **move the code** — often one class is simply in the wrong component, and relocating it removes the edge with no new abstraction at all. Try this before inventing interfaces. ## Practical detection and prevention - Build tools that model the module graph will refuse or warn on cycles (many JVM/Go/Rust/Python toolchains, `dependency-cruiser` in JS/TS, ArchUnit `slices().should().beFreeOfCycles()`, import-linter in Python). - Enforce in CI, not in review. A cycle appears one innocuous import at a time and is exponentially harder to remove later, because by then dozens of call sites depend on it. - Track the graph's **top-down structure**: cycles usually appear when a low-level component reaches back up for a convenience function. ## Nuance and honest caveats - **Class-level cycles inside one component are fine.** Mutual references between two classes in the same package are normal and often natural (a parent/child pair). ADP is about *release units*. Blanket "no class ever references back" rules cause harm. - **The graph is not fixed.** Martin's point is that the component graph is a map of *buildability and releasability*, and it must be *maintained* as the system grows — expect to re-partition. - **Cycles across services** show up as deploy-order coupling and distributed deadlock-ish rollouts (service A's new endpoint needed by B, whose response A needs). Same principle, higher stakes: fix by versioned, backward-compatible contracts or by extracting a shared contract package/event.

  • Are cycles between classes inside a single package also forbidden by ADP?
    No. ADP governs components — independently releasable units. Mutual class references within one component are often natural; the harm starts when the cycle spans build/release boundaries.
  • How do you choose between inverting an edge and extracting a new component?
    Invert when the coupling is behavioral (one side wants something done); extract when it is shared vocabulary or data types both sides need. If one class is simply in the wrong component, moving it beats both.
  • What does a cycle look like between deployable services rather than libraries?
    Mutual runtime calls that create deploy-order coupling and joint failure: neither can be released or rolled back independently. Fix with backward-compatible versioned contracts, an extracted contract artifact, or asynchronous events that reverse one direction.

Two people who each refuse to leave the room until the other does. Individually reasonable, jointly deadlocked — and the only way out is for one to commit to a rule (an interface) or for a third party to hold the thing they are both waiting on.

saying these in an interview costs you the question

  • Applying ADP to individual classes and banning all mutual references inside a component.
  • Merging the cyclic components into one big component and calling the cycle solved.
  • Extracting a 'common' or 'util' component that then grows dependencies of its own and becomes a new cycle hub.
  • Believing cycles are harmless because 'the compiler handles it'.
  • Thinking cycles only matter for microservices, not for libraries and packages.
  • Relying on manual diagrams or code review instead of automated cycle detection in CI.

context