Skip to main content
Clean Architecture 80 XP · 8 min

ADP: The Acyclic Dependencies Principle

No cycles allowed in the component dependency graph — cycles cause the morning-after syndrome and prevent isolated builds and tests.

Showing
Ad (728×90)

Why this matters

The ADP demands that the component dependency graph be a Directed Acyclic Graph (DAG). The moment a cycle appears — component A depends on B, B depends on C, and C depends back on A — several things break simultaneously. You can no longer build any one of them without first building all the others. You cannot test any one of them in isolation. And you experience the morning-after syndrome: you fix a bug in your component, go home, and the next morning your fix is broken again because someone else's change to a component you indirectly depend on propagated through the cycle.

Martin identifies two techniques for breaking cycles. The first applies DIP: create an interface in the lower-level component that the higher-level component depends on, inverting the direction of the problematic edge. The second extracts a new component that both cyclic components depend on, pushing the shared abstractions into a separate, commonly depended-upon module. Either way, the goal is a graph that can be traversed in topological order — meaning there is always at least one component that nothing else depends on, which you can build, test, and release first.

The problem

A three-module cycle — auth imports user, user imports permissions, permissions imports auth — makes it impossible to build or test any one of them independently.

Bad

# auth/tokens.py
from user.models import User          # auth depends on user

# user/models.py
from permissions.roles import Role    # user depends on permissions

# permissions/roles.py
from auth.tokens import validate_token  # permissions depends on auth — CYCLE!

# To test auth, you must import user.
# To import user, you must import permissions.
# To import permissions, you must import auth.
# You can never test any one of them alone.
// auth/tokens.ts
import { User } from "../user/models";       // auth → user

// user/models.ts
import { Role } from "../permissions/roles"; // user → permissions

// permissions/roles.ts
import { validateToken } from "../auth/tokens"; // permissions → auth  CYCLE!

// Jest/Vitest will report circular dependency warnings.
// A bug in auth also forces re-testing of user and permissions.

The solution

A new auth-types package holds shared abstractions. All three modules depend on auth-types — no module depends on another, forming a clean DAG.

Good

# auth_types/interfaces.py  — abstract types only, no deps on other modules
#   class UserIdentity: ...
#   class TokenValidator(ABC): ...

# auth/tokens.py
from auth_types.interfaces import UserIdentity, TokenValidator  # no cycle

# user/models.py
from auth_types.interfaces import UserIdentity  # no cycle

# permissions/roles.py
from auth_types.interfaces import TokenValidator  # no cycle

# Each module depends only on auth_types — a true DAG.
# auth, user, and permissions can all be tested completely independently.
// auth-types/index.ts  — interfaces only, no dependencies
export interface UserIdentity { id: string; email: string; }
export interface TokenValidator {
  validate(token: string): UserIdentity | null;
}

// auth/tokens.ts
import type { UserIdentity } from "../auth-types"; // auth → auth-types only

// user/models.ts
import type { UserIdentity } from "../auth-types"; // user → auth-types only

// permissions/roles.ts
import type { TokenValidator } from "../auth-types"; // permissions → auth-types only

// DAG: auth-types <- auth, user, permissions — no cycles.
// Each module is testable with a simple mock of auth-types interfaces.

Key takeaway

If you can't test a module without importing the entire application, there's a dependency cycle somewhere. Break it with DIP — create an abstract interface that inverts the problematic edge — or extract shared abstractions into a new component that both parties depend on.

Done with this lesson?

Mark it complete to earn XP and track your progress.