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

The dependency rule

Source-code dependencies must point toward stable abstractions. When concrete modules depend on other concrete modules, changes cascade upward. The dependency rule breaks that rigidity by separating flow-of-control from flow-of-source-dependency.

ARCH Middle ◷ 20 min
Level
FoundationsJuniorMiddleSenior

The B2B order platform had a billing module and an order module. Billing depended on order, which depended on the database access layer, which depended on the PostgreSQL driver. Straightforward — each layer depends on the layer beneath it. Then the business asked for a new payment workflow: billing now needed to call the payment gateway directly, and the payment gateway needed to know about order status to prevent double-charging. The payment gateway module was added and made to depend on order as well. Two months later the team needed to extract billing into a separate service. They could not do it: billing imported order, order imported the database layer, the payment gateway imported order too, and billing had started importing helpers from the payment gateway module as a shortcut. The dependencies formed a web. Every module knew about every other concrete module. Extracting billing meant carrying the entire platform with it. The team had never asked the question that would have prevented this: in which direction should source-code dependencies point?

Two different flows: control and source-code dependency

The confusion that causes most dependency problems is conflating two distinct things that happen simultaneously in code: the flow of control and the flow of source-code dependency.

Flow of control is what executes at runtime. When the billing module calls orderService.getOrder(id), control flows from billing into the order service. This is the execution path — who calls whom.

Flow of source-code dependency is what the compiler sees. When billing has import { OrderService } from '../order/orderService', billing has a source-code dependency on the order module. At compile time, if orderService.ts changes its exported shape, billing must be recompiled. This is the dependency path — who knows about whom.

These two flows are independent. They can point in the same direction, or they can be made to point in opposite directions. The dependency rule is about the second flow, not the first. Control will always flow wherever the runtime calls dictate. But you get to choose which direction knowledge — the import, the requires, the type reference — travels between modules.

The critical question is: if a module changes, which other modules are forced to know about it?

The dependency rule: point toward stability

Robert Martin’s dependency rule, first stated in the context of Clean Architecture, is the principle that formalizes which direction source-code dependencies should point:

Source-code dependencies must point in the direction of stability.

Stability here has a specific meaning: a module is stable if it is unlikely to change. The two forces that make a module stable are:

  • High afferent coupling (Ca): many things depend on it, so it has strong pressure to stay put — changing it breaks too many callers.
  • Abstract, not concrete: it defines an interface or contract, not an implementation. Interfaces do not change because of implementation details; they change only when the contract itself must evolve.

The dependency rule follows from this: if a module A depends on a module B, then whenever B changes, A may be forced to change too. To minimize forced changes, A should depend on the things least likely to change — the stable, abstract things — not on the things most likely to change — the volatile, concrete things.

In the B2B platform, the billing module should depend on the concept of “something that provides order data” — an interface — rather than on the specific PostgresOrderRepository that implements it today. If the implementation switches to a different database, or if the order team refactors the repository internals, billing is untouched. Billing’s dependency is on the contract, which is stable, not on the implementation, which is volatile.

Why this works

Why does pointing toward stability reduce rigidity? Because rigidity — the inability to change one module without cascading changes — is caused by dependencies running from stable modules toward volatile ones. If billing (relatively stable, many things call it) depends directly on the PostgreSQL repository (volatile, implementation details change), then every internal change to the repository forces a potential recompile and recheck of billing and every module billing exports. Reversing this direction — so billing depends only on an interface, and the repository implements that interface — means the repository can change freely as long as the contract holds. Stability becomes a property of where the dependency arrow points, not just of how often the code changes.

Concrete depends on abstract, not the reverse

The practical form of the dependency rule is: concrete things depend on abstract things, never the reverse.

An abstract module (an interface, a protocol, a port in hexagonal terminology) is a contract. It defines what something does without specifying how. It changes only when the contract evolves — which happens rarely and intentionally.

A concrete module (a class, a service, a repository implementation) is an execution unit. It implements the contract using a specific technology, algorithm, or external dependency. It changes whenever the implementation needs to change — which happens frequently.

If a concrete module A depends on another concrete module B, A must change whenever B’s implementation changes. This creates chains of forced changes that ripple across the codebase. The billing module depending on PostgresOrderRepository rather than IOrderRepository is an example: every time the order team changes the Postgres implementation, billing is in the blast radius.

Reversing the direction — concrete depends on abstract — breaks the chain. Billing depends on IOrderRepository. PostgresOrderRepository implements IOrderRepository. Now the dependency chain is:

BillingService → IOrderRepository ← PostgresOrderRepository

Both billing and the Postgres implementation depend on the interface. Neither depends on the other. The implementation can be swapped for a test double, a different database, or a REST API client — and billing does not change.

In the B2B platform, this is why the payment gateway extraction problem happened: there was no abstraction layer. Every module depended directly on every other concrete module. Making concrete modules depend on abstractions is the structural move that would have allowed billing to be extracted cleanly.

lesson.inset.note

The words “stable” and “abstract” are correlated but not identical. A very widely used, very concrete class (like a utility library) might be stable in practice because nobody changes it. But stability-by-convention is fragile — it depends on social agreement, not structural enforcement. Making the dependency point toward an interface makes stability structural: the interface changes only when the contract changes, not when the implementation changes. Structure is more reliable than convention.

Quiz

The billing module contains the line `import { PostgresOrderRepository } from '../order/infra/postgres-order-repository'`. What exactly is wrong with this import, and what is the correct structural fix?

Quiz

At runtime, the BillingService calls orderRepository.getOrder(id), which executes database logic and returns an order. At compile time, BillingService imports IOrderRepository (an interface), and PostgresOrderRepository implements IOrderRepository. Which statement correctly describes the relationship between control flow and source-code dependency here?

Quiz

The team wants to extract the billing module into a standalone service. After following the dependency rule — making all cross-module dependencies point toward interfaces — the extraction takes two days. Before the rule was applied, the same extraction was abandoned after a week because of cascading import chains. What structural property did the dependency rule establish that made extraction feasible?

Recall before you leave
  1. 01
    What is the difference between flow of control and flow of source-code dependency? Why does the dependency rule only govern the second one?
  2. 02
    What does 'stable' mean in the context of the dependency rule, and why does pointing toward stability reduce rigidity?
  3. 03
    In the B2B order platform, before the dependency rule was applied, why did extracting the billing module require carrying the whole platform with it?
Recap

The dependency rule is a discipline about which direction source-code imports point, not about which direction calls flow at runtime. Flow of control (who calls whom) and flow of source-code dependency (who imports whom) are independent. The rule says: source-code dependencies must point toward stable abstractions.

Stability means unlikely to change. An abstract module — an interface, a port, a contract — is structurally stable because it changes only when the contract evolves. A concrete module — a repository implementation, a gateway adapter, a framework class — is volatile because it changes whenever its implementation needs to change. The dependency rule says volatile concrete modules depend on stable abstract ones, never the reverse.

The structural consequence: concrete depends on abstract, abstract never depends on concrete. In the B2B platform, BillingService imports IOrderRepository (abstract, stable). PostgresOrderRepository implements IOrderRepository. Neither depends on the other. The dependency arrow runs: BillingService → IOrderRepository ← PostgresOrderRepository. Control still flows from billing to the repository at runtime; source-code dependency runs from both toward the interface.

Violating this rule — concrete importing concrete — creates chains of forced changes. Fixing it requires introducing an abstraction at every boundary where a volatile module is imported by a module that should be insulated from its implementation. The next lesson examines the mechanism that performs this inversion: the Dependency Inversion Principle.

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 4 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.