What does the Acyclic Dependencies Principle (ADP) state, and which team failure mode — nicknamed "the morning-after syndrome" — does it prevent?
answer
- No cycles in the component graph = DAG
- Morning-after syndrome: overnight breakage
- Release isolation: depend on versions, not working copies
- Cycle = components must ship in lockstep
- Classes may cycle; components may not
basics
~20 sADP 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 sADP: "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
State the principle, define "component" as a releasable unit, and give the morning-after story as the motivation.
Add the concrete costs — lockstep releases, transitive build/test blowup, loss of reuse — and note cycles usually appear indirectly.
Frame ADP as enabling release isolation and versioned dependencies; distinguish compile-time from runtime cycles; mention CI enforcement tooling.
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.