skip to content

Acyclic Dependencies Principle

The component dependency graph must be a DAG, because a cycle means those components can no longer be built, tested or released independently. You will learn to break cycles with dependency inversion or a new extracted component.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

5

What does the Acyclic Dependencies Principle (ADP) state, and which team failure mode — nicknamed "the morning-after syndrome" — does it prevent?

level: juniorimportance: must knowfreq 55%

answer

  1. No cycles in the component graph = DAG
  2. Morning-after syndrome: overnight breakage
  3. Release isolation: depend on versions, not working copies
  4. Cycle = components must ship in lockstep
  5. Classes may cycle; components may not

basics

~20 s

ADP says: allow no cycles in the dependency graph between components (releasable units like packages, modules, or libraries). Cycles cause the "morning-after syndrome": someone edits code you depend on overnight, your work breaks, and because dependencies loop, nobody can ever stabilize.

solid answer

~50 s

ADP: "Allow no cycles in the component dependency graph." A component is a unit you can release and deploy on its own — a package, module, library, JAR/DLL/npm package. Draw an arrow A → B whenever code in A references code in B; that graph must be a directed acyclic graph (DAG). The morning-after syndrome is the failure it prevents: when components depend on each other in loops, any change can break anyone, so developers arrive each morning to find yesterday's working code broken by someone else's overnight edits — chronically, as the system grows. A DAG lets each team build against a *released, versioned* copy of its dependencies and choose when to absorb a new version. In a cycle that is impossible: A cannot cut a stable release without B, and B needs A. Cycles also fuse the whole loop into one thing that must be compiled, tested, and deployed together.

go deeper

for a junior

State the principle, define "component" as a releasable unit, and give the morning-after story as the motivation.

for a middle

Add the concrete costs — lockstep releases, transitive build/test blowup, loss of reuse — and note cycles usually appear indirectly.

for a senior

Frame ADP as enabling release isolation and versioned dependencies; distinguish compile-time from runtime cycles; mention CI enforcement tooling.

for a principal

Position ADP among the component coupling principles (with SDP/SAP), tie acyclicity to team autonomy and deployment topology, and discuss the organizational cost of a component graph nobody owns.

## Vocabulary first - **Component**: the smallest thing you can *release* and *deploy* independently — a package, module, library (JAR, DLL, gem, wheel, npm package), or at larger grain a deployable service. ADP is about components, **not** individual classes. - **Dependency**: component `A` depends on `B` if code in `A` refers to code in `B` (imports, calls, extends its types, links against it). Draw an arrow `A → B`. - **Component dependency graph**: all components as nodes, all those arrows as edges. - **Cycle**: a path of arrows that returns to where it started — `A → B → A`, or longer and sneakier: `A → B → C → D → A`. - **DAG (directed acyclic graph)**: a directed graph with no cycles. ADP restated: *the component dependency graph must be a DAG.* ## The principle > **Allow no cycles in the component dependency graph.** It is one of the three *component coupling* principles (with SDP, the Stable Dependencies Principle, and SAP, the Stable Abstractions Principle), popularized by Robert C. Martin. Cohesion principles (REP/CCP/CRP) decide *what goes in* a component; coupling principles decide *how components may point at each other*. ## The morning-after syndrome The origin story: in a growing team, everyone edits shared code. You go home with green tests. Overnight, someone changes a module you depend on. Next morning your code no longer builds or passes. You fix it — and break someone else. Beyond a certain size the team spends more time chasing each other's changes than delivering. Nobody can point at a "good" version of anything, because a good version of `A` requires a good version of `B`, which requires a good version of `A`. **The classic cure is the weekly build + release isolation.** Teams stop consuming each other's *working copies* and start consuming each other's *released, version-numbered artifacts*: "I depend on `Persistence 1.4.2`." When a new version ships, I read its release notes and migrate **when I choose**. My world stops moving under me. That decoupling is only possible when the graph is acyclic: - In a DAG, a component only needs its dependencies to be releasable to be releasable itself. You can always find leaf components with no dependencies, release them, then release their dependents, and so on. - In a cycle, `A` can't be validated without `B`, and `B` can't be validated without `A`. They are effectively **one component wearing several hats** — a "big ball of mud" with directory separators pretending to be structure. ## What a cycle actually costs 1. **Release lockstep** — every component in the loop must be versioned and shipped together. Independent deployability, the whole point of componentization, disappears. 2. **Build/test blowup** — to test `A` you must compile `B`, `C`, `D`… and transitively everything they reach. Test suites get slower and less isolated; unit tests drag in half the system. 3. **Change amplification** — a change in any member of the cycle can ripple to all the others, in both directions. Reasoning becomes global rather than local. 4. **No reuse** — you cannot take `A` into another product without dragging the entire loop. 5. **Ownership fog** — two teams owning two components in a cycle are permanently coupled to each other's schedules. ## Important nuances - **Cycles between classes inside one component are fine.** Mutual recursion, back-references, observer callbacks — ADP is silent on those; they ship together anyway. The principle constrains the *component* graph. - **Cycles are usually created by accident and indirectly.** Nobody writes `A → B → A` on purpose. Someone adds one innocent import (`Utilities` needs one enum from `Reporting`) and a five-node cycle appears that no single reviewer saw. That is exactly why it must be checked by a tool in CI, not by eyeball. - **ADP is not "minimize dependencies".** A DAG can be dense. ADP only forbids *loops* in the direction of dependency. - **Build-time vs runtime.** ADP is about *source/compile/deploy-time* dependency. Runtime call graphs may legitimately loop back via callbacks, events, or dependency injection — indeed inverting a compile-time arrow while keeping the runtime call direction is the standard fix (see DIP). ## How to check it Any reachability/strongly-connected-component analysis over the import graph: `jdeps`/ArchUnit/Spring Modulith (JVM), `dependency-cruiser`/`madge` (JS/TS), `import-linter` (Python), `go list`/language-enforced acyclicity (Go forbids import cycles outright), NDepend (.NET). Make it a build-breaking rule, so the graph can never quietly regress.

  • If cycles are so damaging, why do they keep appearing in real codebases?
    Because they emerge indirectly: a single new import can close a loop several hops long that no reviewer can see in a diff. The graph is emergent, not designed, so it needs an automated check in CI rather than discipline alone.
  • Does ADP forbid two classes in the same package referring to each other?
    No. ADP constrains the graph between components (independently releasable units). Inside a component, mutual references are normal — the whole component ships as one unit anyway.

Think of components as suppliers in a manufacturing chain. If the engine plant needs finished cars to build engines, and the car plant needs engines to build cars, neither can ever ship a first unit — they must retool as one giant factory. A one-directional supply chain lets each plant certify a batch, stamp a version on it, and let the next plant pull it when ready.

saying these in an interview costs you the question

  • Saying ADP means "reduce the number of dependencies" — it forbids loops, not density.
  • Claiming any mutual reference between two classes violates ADP.
  • Believing cycles are always direct A ↔ B; most real ones are long and indirect.
  • Thinking a cycle is harmless "because it compiles" — the cost is in release, test isolation, and change ripple, not in the compiler.
  • Confusing runtime call cycles (often fine, e.g. callbacks) with compile-time dependency cycles.

context

open as a page

Components A, B and C form a dependency cycle: A → B → C → A. Describe the two standard techniques for breaking such a cycle and how each changes the graph.

level: middleimportance: must knowfreq 50%

basics

~20 s

Two options. (1) Apply the Dependency Inversion Principle: put an interface that C needs into C (or a place C already sees), and have A implement it — the arrow A → B → C → A becomes C ← A with no return edge. (2) Extract the shared code into a new component D that both A and C depend on.

open as a page

How do you derive a valid build-and-release order for a set of components from their dependency graph, and why does a single cycle make such an order impossible?

level: middleimportance: should knowfreq 40%

basics

~20 s

Topologically sort the dependency graph: build components with no unbuilt dependencies first, then their dependents, and so on. A cycle has no such starting point — each member waits on another — so no valid order exists and the whole loop must be built and released as one unit.

open as a page

Dependency cycles usually appear indirectly, from one seemingly harmless import. How would you detect, prevent and govern cycles in a growing codebase?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Don't rely on code review — a loop can span five components and be invisible in a diff. Add an automated dependency check to CI that fails the build on any cycle (ArchUnit/Spring Modulith, dependency-cruiser, import-linter, jdeps, NDepend), fix violations immediately, and keep a documented, owned component map.

open as a page

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%

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.

open as a page