Be Software Architecture Wise
Be Wise Apps

Leer en español · Lire en français · Auf Deutsch lesen

Section 3.4 of 3.8 · Intermediate · 25 min read

Managing dependencies: direction, cycles and stability

Video lesson

5 min · narrated, with captions
Listen only (audio)
Read the transcript

Every dependency has a direction, and that direction decides who is disturbed by whose changes. In this lesson you'll learn three principles for dependencies between components, the metrics behind them, and how to check them automatically.

Think of a building. The upper floors rest on the lower ones, and the foundations rest on nothing above them. You can redecorate the top floor without touching the foundations. But if you dig under the foundations, everything above is at risk. Now imagine two floors that each hold the other up. You could never rebuild either. Good software puts parts that change often on top of solid, rarely changed parts, and never lets two parts hold each other up in a loop.

Draw a dependency as an arrow from the module that needs something to the module it needs: Checkout to Pricing. If Pricing changes, Checkout may have to change. If Checkout changes, Pricing doesn't care. Changes travel against the arrows. Between services, direction shows up as knowledge. If Orders calls Billing's API, Orders depends on Billing. If Orders publishes an event that Billing consumes, the arrow reverses, and Orders doesn't even know Billing exists.

Robert C. Martin's first principle is the Acyclic Dependencies Principle: no cycles between components. Here, Orders calls Notifications to send a confirmation, and Notifications calls back into Orders to read the order. Neither can be built, tested or released without the other. The first fix inverts one dependency. Notifications declares a small interface, order lookup, describing exactly what it needs, and Orders implements it. Notifications now depends on nothing in Orders. The interface belongs to the module that uses it. The second fix extracts what both sides need, here the order events, into a new component that both depend on. It depends on neither, so it's more stable than either.

The second principle is the Stable Dependencies Principle: depend in the direction of stability. A component meant to be easy to change shouldn't be depended on by one that's hard to change. Martin measures this as instability: the outgoing dependencies, divided by all dependencies, incoming plus outgoing. Zero means only incoming dependencies: maximally stable, and responsible to many. One means only outgoing: nothing depends on it, so it's free to change. So along every arrow, instability should fall. Each component should depend on components more stable than itself.

The third principle is the Stable Abstractions Principle: a component should be as abstract as it is stable. Abstractness is the share of its classes that are abstract classes or interfaces. Plot abstractness against instability and you get this square. The diagonal is the main sequence: stable and abstract at the top left, unstable and concrete at the bottom right. Distance from it runs from zero, on the line, to one, in a far corner. Near the bottom left is the zone of pain: stable, because much depends on it, yet concrete, so changes are necessary and painful. A database schema every module reads directly lives here. Near the top right is the zone of uselessness: abstractions nothing depends on, often interfaces left behind after a refactoring.

Let's try it on a shop. Its payments API component has six incoming dependencies and one outgoing, so instability is one seventh, about nought point one four. Four of its five classes are interfaces. It sits almost exactly on the main sequence. Orders core has eight incoming and two outgoing, so instability is nought point two. But only two of its twenty classes are abstract, so its distance from the main sequence is nought point seven. Stable, but concrete: it's in the zone of pain. The web user interface depends on nine others and nothing depends on it. Fully unstable, fully concrete, and right on the line. The remedy moves the types that orders core's dependents use into a small, mostly abstract component, and leaves the concrete code in a separate one. Both end up at a distance of nought point two five. The numbers are prompts to look, not targets.

Layering is the oldest dependency rule. In strict layering, each layer may use only the layer directly below it. Each layer hides everything beneath it, but middle layers may just forward calls. In relaxed layering, a layer may use any layer below it. That removes the forwarding code, but a change at the bottom can affect every layer above. Whatever rules you choose, check them automatically. Tools such as ArchUnit for Java, dependency-cruiser for JavaScript and import-linter for Python run in the build and fail it when a rule is broken.

The full section has example rules for each tool and a decision guide for fixing an unwanted dependency. Next, in Section three point five, we apply the SOLID principles to components and services. See you there.

What you need to know

Why direction matters

A dependency exists when one module needs another to compile, run or behave correctly. Draw it as an arrow from the module that depends to the module it depends on: Checkout to Pricing. The arrow tells you something important about change. If Pricing changes its interface or behaviour, Checkout may have to change; if Checkout changes, Pricing does not care. Changes travel against the direction of the arrows. So the direction of every dependency is a decision about who will be disturbed by whose changes.

Architects care about two kinds of direction that are easy to confuse. The flow of control is who calls whom at run time. The source-code dependency is who imports, references or compiles against whom. Usually they point the same way, but they need not. A framework calls your code without depending on it (Section 0.3 called this inversion of control), and with an interface, a high-level module can call a low-level one while the source-code dependency points the other way. That freedom to choose the direction of the source-code dependency independently of the flow of control is the most powerful tool in this section.

Between services, direction shows up as knowledge. If Orders calls Billing's API, Orders depends on Billing and must change when Billing's API changes. If Orders publishes an event that Billing consumes, the dependency is reversed: Billing depends on Orders' event contract, and Orders does not know Billing exists (Section 3.2). Choosing between a call and an event is therefore partly a choice about dependency direction.

The Acyclic Dependencies Principle

Robert C. Martin formulated three principles for dependencies between components, first in articles and papers in the mid-1990s and later in Agile Software Development: Principles, Patterns, and Practices (2002) and Clean Architecture (2017). The first, the Acyclic Dependencies Principle (ADP), says that the dependency graph between components must contain no cycles: it must be a directed acyclic graph.

A dependency cycle such as Orders depends on Notifications, Notifications depends on Customers and Customers depends on Orders turns three components into one. None can be compiled or tested without the others; a change to any can ripple round the loop back to itself; none can be released on its own; and nobody can say which to build first. Martin introduces the principle with what he calls the morning after syndrome: you go home with working code and return the next day to find it broken because someone else changed something you depend on. His cure is to split the system into separately released components, and that works only if the dependencies between them contain no cycles. Cycles also make it impossible to extract one component later as a library or service, because it drags the whole loop with it.

The Java Platform Module System enforces ADP between modules: the Java Language Specification makes it a compile-time error for a module to depend, directly or indirectly, on itself. Most other languages allow cycles between packages, so teams check for them with tools. Cycles between classes inside one small component are less harmful, and Martin Fowler, among others, tolerates them there; the principle matters most between components that different people build and release.

Breaking cycles

Every cycle can be broken, and there are two standard techniques. Both are shown in the diagram below for a cycle in which Orders calls Notifications to send a confirmation and Notifications calls back into Orders to read the order's details.

Dependency inversion. Notifications declares an interface describing exactly what it needs, say OrderLookup with one method, and Orders implements it. The arrow from Notifications to Orders now points the other way: Orders depends on Notifications (to call it and to implement its interface), and Notifications depends on nothing in Orders. This is dependency inversion, which Section 3.5 covers as the D in SOLID. The key detail is ownership: the interface belongs to the module that uses it, not to the one that implements it.

Extract a new component. Find what both sides actually need from each other, often a small shared model or contract, and move it into a new component that both depend on. Here, Orders publishes an event type defined in an order events component, and Notifications reads the order details from the event instead of calling back. The new component is depended on by both and depends on neither, so it is more stable than either: exactly what the next principle asks for.

Two ways to break a cycle

A dependency cycle between Orders and Notifications, broken in two ways: by dependency inversion and by extracting a new componentCycleOrdersNotificationssendsreads orderneither can be built, testedor released without the otherFix 1: invert one dependencyOrdersOrderLookupNotificationsimplementsinterface owned by NotificationsFix 2: extract a componentOrdersNotifi-cationsOrder events(shared model)both depend on a new,more stable componentFix 1: Orders calls Notifications, which states what it needs as an interface; Orders implements it.Fix 2: the part both needed moves to its own component; Notifications no longer reads Orders.Arrows point from the module that depends to the module it depends on. Dashed: implements.
A cycle can be broken by inverting one dependency through an interface owned by its user, or by extracting what both sides share into a new component.

The Stable Dependencies Principle and instability

Some components change often (user interfaces, integrations with partners, new features); others should change rarely (core domain concepts, shared contracts, money handling). The Stable Dependencies Principle (SDP) says: depend in the direction of stability. A component that is meant to be easy to change should not be depended on by a component that is hard to change, or it will become hard to change too.

Martin measures stability through dependencies, not through how often the code is edited. A component that many others depend on is stable in the sense that it is costly to change: every change risks breaking its dependents. A component that depends on many others and has no dependents is unstable: nothing stops it from changing, and its dependencies may force it to. Using afferent coupling (Ca, incoming) and efferent coupling (Ce, outgoing) from Section 3.2, instability is:

I = Ce / (Ca + Ce)

I ranges from 0 to 1. I = 0 means only incoming dependencies: maximally stable, responsible to many, depending on nothing. I = 1 means only outgoing dependencies: maximally unstable, nothing depends on it. SDP, stated as a metric, says that along every dependency arrow I should decrease: each component should depend on components with a lower I than its own. A dependency from a stable component (I = 0.1) to a volatile one (I = 0.8) is a warning sign; the usual fix is again dependency inversion, putting an interface in a stable place.

The Stable Abstractions Principle and the main sequence

Stability has a cost: a component with I near 0 is hard to change. If it is full of concrete code, the system becomes rigid. The Stable Abstractions Principle (SAP) resolves this: a component should be as abstract as it is stable. Stable components should consist mostly of interfaces and abstract types, so that their behaviour can be extended by new implementations elsewhere without changing them; unstable components can be concrete, because they are easy to change anyway. SDP and SAP together amount to the dependency inversion principle applied to whole components: dependencies point towards stability, and stability goes with abstraction.

Abstractness is measured as A = Na / Nc, where Na is the number of abstract classes and interfaces in the component and Nc the total number of classes. A = 0 means fully concrete; A = 1 means nothing but abstractions. Plotting A against I gives a square. The line from (I = 0, A = 1) to (I = 1, A = 0) is the main sequence: stable and abstract at one end, unstable and concrete at the other, with balanced mixtures between. Distance from it is:

D = |A + I - 1|

D ranges from 0 (on the line) to 1 (in a far corner). The corner near (0, 0) is the zone of pain: stable, because much depends on it, and concrete, so any change is both necessary and painful. A database schema that every module reads directly lives here. Martin notes that some components belong there harmlessly, such as a mature concrete utility library that never needs to change; the zone hurts only for components that are volatile. The corner near (1, 1) is the zone of uselessness: abstractions that nothing depends on, typically interfaces left behind after a refactoring or designed for an extension that never came.

The main sequence

Abstractness plotted against instability, with the main sequence from (0,1) to (1,0), the zone of pain near (0,0) and the zone of uselessness near (1,1), and three components plotted0101Instability I = Ce / (Ca + Ce)Abstractness AZone ofpainZone ofuselessnessmain sequence: A + I = 1Componentspayments-apiI 0.14, A 0.80D 0.06orders-coreI 0.20, A 0.10D 0.70web-uiI 1.00, A 0.00D 0.00red dot: off themain sequencepayments-apiorders-coreweb-ui
Components near the main sequence balance abstractness against stability; the zone of pain holds stable concrete code, and the zone of uselessness holds abstractions nobody uses.

Worked example

Worked example: computing I, A and D for three components

A shop has three components. Following Martin's definition, Ca counts classes outside the component that depend on classes inside it, and Ce counts classes inside it that depend on classes outside. The counts come from a dependency tool.

payments-api (5 classes, 4 of them interfaces)Ca = 6, Ce = 1. I = 1 / (6 + 1) = 0.14. A = 4 / 5 = 0.80. D = |0.80 + 0.14 - 1| = 0.06.
orders-core (20 classes, 2 abstract)Ca = 8, Ce = 2. I = 2 / (8 + 2) = 0.20. A = 2 / 20 = 0.10. D = |0.10 + 0.20 - 1| = 0.70.
web-ui (12 classes, none abstract)Ca = 0, Ce = 9. I = 9 / (0 + 9) = 1.00. A = 0 / 12 = 0.00. D = |0 + 1 - 1| = 0.00.
Check SDP along the arrowsweb-ui (1.00) depends on orders-core (0.20) and on payments-api (0.14); orders-core (0.20) depends on payments-api (0.14). I decreases along every arrow, so SDP holds.
Diagnosisorders-core has D = 0.70: stable (many dependents) but concrete. If its rules change often, every change risks eight dependent classes. It is in the zone of pain.
Remedy and re-computationExtract the four types its dependents use into orders-api (3 interfaces and 1 value type: A = 0.75; Ca is 8 or more, since the implementing classes now depend on it too, and Ce = 0, so I = 0; D = |0.75 + 0 - 1| = 0.25). The remaining 16 concrete classes become orders-impl, depended on only by the start-up wiring (Ca = 1) and depending on orders-api and two others (Ce = 3): I = 3 / 4 = 0.75, A = 0, D = 0.25.

The remedy moves both parts much closer to the main sequence (0.70 to 0.25 each). Dependents now rely on a small, stable, mostly abstract component, and the volatile concrete code sits where it is cheap to change.

The numbers are heuristics, not targets. A D of 0.70 is a reason to look, not proof of a problem; if orders-core never changed, it would do no harm where it is.

Strict and relaxed layering

Layering is the oldest and most common dependency rule: arrange modules in layers, such as presentation, application or domain, and persistence, and allow each layer to use only layers below it. Lower layers know nothing about higher ones, so a new user interface can be added without changing the domain. Layering is an architectural style in its own right (Section 5.2); here the interest is the dependency rule it imposes.

In strict (closed) layering, each layer may use only the layer immediately below it. Each layer then hides everything beneath it, and replacing a lower layer affects only one other layer; the price is pass-through code, where the middle layer merely forwards calls it has nothing to add to. In relaxed (open) layering, a layer may use any layer below it. That removes the boilerplate but means a change to the bottom layer can affect every layer above. Many real systems mix the two, marking specific layers as open, such as a shared logging or utilities layer that everyone may use.

Classic layering has one notorious weakness: the domain layer depends on the persistence layer, so the most important business rules depend on database details. Hexagonal, onion and clean architecture (Section 5.6) apply dependency inversion to fix it: the domain owns interfaces such as OrderRepository, and the persistence code implements them, so all source-code dependencies point inwards towards the domain.

Strict and relaxed layering

Strict layering allows calls only to the layer directly below; relaxed layering allows calls to any lower layerStrict (closed) layersPresentationApplication / domainPersistenceeach layer uses only the next one downRelaxed (open) layersPresentationApplication / domainPersistenceskipany layer may use any layer below it
Strict layering allows calls only to the next layer down; relaxed layering also allows a layer to skip layers below it.

Checking dependency rules automatically

Dependency rules written only in a wiki are broken within weeks. The reliable way to keep them is to express them as executable checks that run in the build and fail it when a rule is violated. Such a check is an architectural fitness function, an idea introduced in Section 2.8 and developed in Section 11.3. Three widely used open-source tools cover the main ecosystems.

ArchUnit (Java and other JVM languages) lets you write architecture rules as ordinary unit tests: which packages may access which, layer definitions, and freedom from cycles between slices of the code base. dependency-cruiser (JavaScript and TypeScript) analyses imports and validates them against rules in a configuration file, and its starter configuration already forbids circular dependencies. import-linter (Python) checks contracts declared in a configuration file; its contract types include forbidden imports, layers, independence between sibling modules, protected modules and acyclic siblings.

@ArchTest
static final ArchRule layers = layeredArchitecture()
    .consideringAllDependencies()
    .layer("Web").definedBy("..web..")
    .layer("Domain").definedBy("..domain..")
    .layer("Persistence").definedBy("..persistence..")
    .whereLayer("Web").mayNotBeAccessedByAnyLayer()
    .whereLayer("Domain").mayOnlyBeAccessedByLayers("Web", "Persistence")
    .whereLayer("Persistence").mayNotBeAccessedByAnyLayer();

@ArchTest
static final ArchRule noCycles =
    slices().matching("com.shop.(*)..").should().beFreeOfCycles();
ArchUnit (Java): a layering rule and a cycle check written as tests.
// .dependency-cruiser.js
module.exports = {
  forbidden: [
    { name: "no-circular", severity: "error",
      from: {}, to: { circular: true } },
    { name: "domain-not-to-db", severity: "error",
      comment: "Domain code must use repository interfaces, not the database",
      from: { path: "^src/domain" },
      to: { path: "^src/db" } },
  ],
};
dependency-cruiser (JavaScript and TypeScript): forbid cycles and stop the domain importing the database layer.
# .importlinter
[importlinter]
root_package = shop

[importlinter:contract:layers]
name = Web above domain above adapters
type = layers
layers =
    shop.web
    shop.domain
    shop.adapters

[importlinter:contract:features-independent]
name = Feature packages do not import each other
type = independence
modules =
    shop.catalogue
    shop.ordering
    shop.payments
import-linter (Python): a layers contract and an independence contract, checked by running lint-imports in the pipeline.

Notice that the ArchUnit rule above lets Persistence access Domain but not the other way round: the domain-centred arrangement just described, not classic layering. Rules like these are cheap to add, run in seconds, and turn an architecture diagram into something the build defends. When an existing code base already violates a rule, most tools can record the current violations as a baseline and fail only on new ones, so the rule stops things getting worse from the day it is introduced.

Decision guide

Decision guide: what to do about an unwanted dependency

Use this when a dependency check fails or a review finds a dependency that points the wrong way.

  1. Is the dependency part of a cycle between components?
    YesBreak the cycle first (ADP): continue below to choose how.
    NoGo to the stability question.
  2. Does one side need only a small, well-defined service from the other?
    YesInvert the dependency: let the side that uses the service own an interface, and let the other side implement it.
    NoGo to the next question.
  3. Do both sides depend on a shared model, contract or piece of logic?
    YesExtract it into a new component that both depend on, and make that component small, stable and mostly abstract.
    NoConsider whether the two components are really one: merge them, then re-split along a better boundary (Section 3.1).
  4. Does a stable component (low I) depend on a volatile one (high I)?
    YesThis breaks SDP. Put an interface in the stable component, or in a new stable one, and move the volatile code behind it.
    NoThe direction is acceptable. Record the rule in a fitness function so it stays that way.

How professionals manage dependencies

Experienced architects keep a picture of the dependency graph between components and look at it regularly, generated from the code rather than drawn by hand. They decide which components are meant to be stable (domain concepts, shared contracts) and which volatile (user interfaces, integrations), and make the arrows point from volatile to stable. They treat new cycles as build failures, not as style issues. And they apply the same thinking across services: shared libraries and shared event schemas are the stable components of a distributed system, deserving minimal surfaces and careful versioning, while each service's internals stay free to change.

None of this needs the metrics to be computed precisely. The value is in the questions they force: which way should this arrow point, what happens to everything upstream if this changes, and is this component as abstract as the number of things that depend on it requires? Section 3.5 continues with the SOLID principles, which apply the same ideas inside components and between them.

Check your understanding

Why must every component in a dependency cycle be released together?

Each depends, directly or indirectly, on all the others, so a change to any can require changes to the rest; none can be built or tested without the others.

A component has Ca = 3 and Ce = 9. What is its instability, and what does it mean?

I = 9 / 12 = 0.75. It depends on much more than depends on it, so it is relatively unstable: easy to change, but exposed to its dependencies' changes.

What is in the zone of pain, and when does it actually hurt?

Components that are both stable (many dependents) and concrete (few abstractions). It hurts when such a component needs to change often; a concrete component that never changes does little harm there.

What is the difference between strict and relaxed layering?

In strict layering a layer may use only the layer directly below it; in relaxed layering it may use any layer below it.

Next · 3.5SOLID at scaleThe five SOLID principles explained precisely, each shown in Python and then at component and service scale, plus the REP, CCP and CRP cohesion principles, their tension triangle and the honest criticism of SOLID.Continue →

Sources and further reading

Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.

Ask: “How do I find and break the dependency cycles in a large codebase without stopping feature work?”