open atlas
↑ Back to track
Architecture Patterns ARCH · 02 · 03

Acyclic and stable abstractions

ADP, SDP, and SAP extend the dependency rule to the full component graph. ADP: no cycles. SDP: depend toward stability. SAP: stable components must be abstract. Together they define the main sequence and the zones of pain and uselessness.

ARCH Middle ◷ 24 min
Level
FoundationsJuniorMiddleSenior

Six months after the B2B order platform was cleaned up with DIP, the team ran a dependency graph analysis and found something unexpected. The billing component and the order component were cleanly separated — no direct imports between them. But the graph showed a cycle: billing imported the shared-utils component (for money formatting), shared-utils imported the order component (for the OrderStatus enum it had absorbed over time), and order imported billing (for invoice reference validation). None of these three components imported each other directly. The cycle ran through a shared library that nobody owned. Every time anyone in the cycle changed anything — even a formatting helper — all three components had to be redeployed together. The team had achieved DIP at the bilateral level and still built a distributed big ball of mud at the component graph level. The three principles in this lesson operate at that higher level: not the direction of one dependency arrow, but the shape of the entire dependency graph.

ADP: Acyclic Dependencies Principle

The Acyclic Dependencies Principle is simple to state: there must be no cycles in the component dependency graph. A cycle means a set of components where each eventually depends on the others — directly or transitively. The opening story is a classic example: billing → shared-utils → order → billing.

Cycles are destructive for three reasons:

They turn independent components into one release unit. If A depends on B depends on C depends on A, you cannot build or deploy any of them without the others. They must be released as a group even though they were designed as separate components. This is the “morning after” problem: a change in any component in the cycle can break the others, so the team cannot integrate incrementally.

They make it impossible to reason about change impact. In an acyclic graph, you can follow dependency arrows to identify exactly which components a change can affect — the set is finite and directed. In a cycle, a change in any node affects every other node in the cycle. The blast radius is the entire cycle.

They force higher coupling metrics. Every component in the cycle effectively has a source-code dependency on every other component’s internals. The afferent/efferent coupling metrics become meaningless because no component can be separated from the others to be analyzed independently.

Breaking cycles: two strategies

When you detect a cycle, you have two structural tools:

1. Apply DIP to invert one of the dependency arrows. If A → B → C → A is the cycle, introduce an interface in A that C implements. The arrow C → A becomes C → IA (where IA lives in A’s package), and A imports only IA. The cycle is broken: A → B → C → IA, with C implementing IA. No node depends on A as a concrete module.

2. Extract a new component that both sides can depend on. If A and C depend on each other because they share something (the OrderStatus enum, in the opening story), extract that shared thing into a new component D that has no dependencies. Then A → D, C → D. Both A and C depend on D; neither depends on the other. The cycle disappears because D is a leaf node.

In the opening story, the fix was option 2: extract the OrderStatus enum from shared-utils into a new order-domain-types component with no dependencies. Billing imports order-domain-types directly. Order imports order-domain-types. Shared-utils no longer imports order. The cycle is gone.

SDP: Stable Dependencies Principle

The Stable Dependencies Principle extends the dependency rule from the previous lessons to the component level: depend only in the direction of stability. A component should only depend on components that are at least as stable as itself.

Stability at the component level is measured by the instability metric from unit 01:

I = Ce / (Ca + Ce)

where Ce is the count of outgoing dependencies (efferent) and Ca is the count of incoming dependencies (afferent). I = 0 is maximally stable (nothing it depends on can force it to change; many things depend on it). I = 1 is maximally unstable (it depends on many things, and nothing depends on it).

SDP says: if component A has instability I_A and depends on component B with instability I_B, then I_A ≥ I_B must hold. The arrow should point from higher instability to lower instability — from more volatile toward more stable.

The failure mode of violating SDP: a volatile component is depended on by a stable component. The stable component was designed to be hard to change. But now it has a dependency on something volatile. The volatile component changes, and it forces the stable component to change — even though the stable component has high afferent coupling and breaking it is expensive. This is exactly how a well-designed core domain module becomes fragile: someone adds a dependency from it to a utility that is changed frequently.

In the B2B platform, the billing engine (Ca = 12, I ≈ 0.08 — very stable) should depend only on components with similarly low instability: the IOrderPort interface (I = 0 — pure abstraction, nothing depends on it from the other direction), the money utility (I = 0.1 — stable, many dependents). It should not depend on the reporting utilities component (I = 0.9 — almost nothing depends on it, it depends on many things, it changes weekly). If billing imports reporting utilities, a weekly reporting change now forces a billing recheck.

Why this works

Why does SDP violation feel so bad in practice? Because it surprises people. A module with I ≈ 0 is supposed to be the bedrock — the thing that never changes and that everyone can depend on. When it breaks on a Tuesday because a colleague changed a logging helper it should never have imported, the violation of the mental model is jarring. SDP makes that mental model structural: the dependency arrows encode the stability hierarchy, so the bedrock module’s low instability is enforced by its outgoing dependency set, not just by convention.

SAP: Stable Abstractions Principle

The Stable Abstractions Principle is the partner to SDP: a component should be as abstract as it is stable. Stability and abstractness must scale together.

This is not an aesthetic preference — it follows from the dependency rule. If a component is stable (high Ca, low I), many things depend on it. Those dependents cannot easily change it when their needs evolve. The only way the stable component can remain useful as requirements change without forcing changes in all its dependents is if it is abstract: defined as interfaces and abstract types that can be extended without modification. A stable, concrete component cannot be extended without being changed — and changing it breaks all its dependents.

The two failure zones follow directly:

Zone of Pain (stable + concrete, I ≈ 0, A ≈ 0): A component that is highly stable (many dependents, hard to change) but entirely concrete (all implementation, no interfaces). If this component must ever change, every dependent must change too. The concrete infrastructure code that every module in the platform imports but that must be updated with each framework upgrade lives here. It is painful because you cannot extend it without breaking dependents, and you cannot stop depending on it because everything already does.

Zone of Uselessness (unstable + abstract, I ≈ 1, A ≈ 1): A component that is entirely abstract (all interfaces, no implementation) but that nobody depends on. These interfaces were probably written for a future use case that never arrived. They are useless because no concrete component implements them, and no component depends on them — they contribute nothing to the running system.

The main sequence is the diagonal line between these two zones: the ideal position for a component is where its abstractness roughly equals its instability (A ≈ I). A maximally stable component (I = 0) should also be maximally abstract (A = 1) — a pure interface package. A maximally unstable component (I = 1) should be maximally concrete (A = 0) — a leaf implementation that implements interfaces and depends on nothing. Everything else should fall on or near the diagonal.

Applying the three principles together

ADP, SDP, and SAP are most powerful when applied together as a complete constraint set for the component graph:

  1. Run ADP first: detect and break cycles. A cyclic graph invalidates instability metrics (Ca/Ce) because components in a cycle cannot be measured independently.

  2. Compute instability metrics after ADP is clean: with an acyclic graph, measure I for each component.

  3. Apply SDP: check that every dependency arrow points from higher I to lower I. Dependencies that violate this (stable depending on volatile) are the first candidates for DIP-based inversion.

  4. Apply SAP: plot components on the I/A plane. Components far from the main sequence are structural debt: zone-of-pain components need interfaces extracted from them; zone-of-uselessness components need to either be implemented or deleted.

In the B2B order platform after the cycle fix, the remaining structural debt was a shared-utils component sitting in the zone of pain: I ≈ 0.05 (many things depend on it) but A = 0 (pure concrete utility functions). The fix was to extract the interfaces that callers actually needed into a small, pure-abstract platform-contracts package, and demote the concrete utilities to a component that only leaf modules depended on.

lesson.inset.note

The main sequence is a target, not a requirement. Some components legitimately sit off the diagonal — a pure configuration class with no dependents and no abstractions (I ≈ 1, A = 0) is fine because it has no structural impact. The zones of pain and uselessness are warnings, not prohibitions. Use the I/A plot to identify the largest deviations and fix those first, rather than trying to optimize every component to the diagonal simultaneously.

Quiz

The dependency graph shows: OrderModule → BillingModule → SharedUtils → OrderModule. The team wants to break the cycle. Which two structural strategies are available, and when is each preferred?

Quiz

The billing engine component has Ca = 15 (15 components depend on it) and Ce = 1 (it depends only on a pure-abstract IPaymentPort interface). By the Stable Abstractions Principle, what should be true about the billing engine's component, and does the described setup satisfy it?

Quiz

A reporting component has I = 0.95 (almost nothing depends on it; it depends on many things) and A = 0.9 (almost entirely interfaces, no concrete logic). Where does it fall on the I/A plane, and is this a structural problem?

Recall before you leave
  1. 01
    Why must ADP be applied before SDP and SAP metrics are computed?
  2. 02
    What are the Zone of Pain and Zone of Uselessness, and what structural move fixes each?
  3. 03
    What does the main sequence represent, and how do you use the I/A plot to prioritize architectural work?
Recap

ADP, SDP, and SAP are three component-level principles that extend the dependency rule to the whole-codebase level.

The Acyclic Dependencies Principle says the component dependency graph must be a directed acyclic graph (DAG). Cycles force components into joint release units, inflate blast radius, and corrupt stability metrics. Break cycles using DIP (invert one arrow with an interface) or extraction (pull shared elements into a new leaf component with no dependencies).

The Stable Dependencies Principle says dependency arrows must point from higher instability toward lower instability. Instability is I = Ce/(Ca+Ce): zero is maximally stable, one is maximally volatile. A stable component depending on a volatile one violates SDP — it means the bedrock of the architecture can be broken by a change in something designed to be replaceable. Fix SDP violations by applying DIP at the violating edge.

The Stable Abstractions Principle says abstractness must scale with stability. A stable component (I ≈ 0) that is also concrete (A ≈ 0) sits in the Zone of Pain: hard to change, impossible to extend without breaking dependents. An unstable component (I ≈ 1) that is also entirely abstract (A ≈ 1) sits in the Zone of Uselessness: all contracts, no implementations. The main sequence (A ≈ I) is the target. Distance from the main sequence quantifies architectural debt and prioritizes structural refactoring work.

Applied together — ADP first, then SDP and SAP — these three principles give you a complete, measurable vocabulary for evaluating and improving the shape of an entire component dependency graph.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 3 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.