2.2.8 · D2Design Principles

Visual walkthrough — SOLID — Dependency Inversion Principle

2,160 words10 min readBack to topic

Before any code, we agree on one drawing convention. Everything that follows is built from it.

Step 1 — Read the one symbol we use: the dependency arrow

WHAT. In every picture on this page a box is a module (a chunk of code — one class, or a package of classes). An arrow is read " depends on " — meaning mentions by name in its source code. If you delete , then stops compiling. That is the whole meaning of the arrow: "cannot exist without."

WHY this symbol first. The entire principle is a statement about which way one arrow points. If we do not fix precisely what the arrow means, "inverting" it is gibberish. So we earn the arrow before we ever draw a system.

PICTURE. The arrow points from the needer to the needed. Notice the arrowhead lands on the thing that must already exist.

Figure — SOLID — Dependency Inversion Principle

Step 2 — Draw the natural, un-inverted system

WHAT. We write the most obvious code: the service that decides what to do directly creates the database object that knows how to store. In code this is the fatal line new MySqlOrderDb().

WHY. Before improving anything we must honestly draw the default. The word "inversion" only has meaning relative to a starting direction, and this is that starting direction: the high-level box reaches down and grabs a concrete detail.

PICTURE. One arrow, pointing down, from policy to detail.

Figure — SOLID — Dependency Inversion Principle

Step 3 — Colour the boxes by how often they change

WHAT. We paint each box by volatility — how frequently we expect its source to change. Business policy changes rarely (green, stable). Database choice changes often (red, volatile — today MySQL, tomorrow Postgres, in tests an in-memory fake).

WHY this step. DIP is not about aesthetics; it is a rule about protecting stability from volatility. To apply it we must first see which box is stable and which is volatile. The rule we are heading toward: arrows should point toward the stable colour, never away from it.

PICTURE. The Step 2 arrow now runs from green into red — from stable into volatile. That is the wrong direction, and the colours make the wrongness obvious.

Figure — SOLID — Dependency Inversion Principle

Step 4 — Insert an abstraction between them

WHAT. We introduce a third box: an interface called OrderRepository. An interface is a contract with no body — a list of method names and their shapes (e.g. save(Order)), and zero implementation. It is a promise, not a doer.

WHY an interface, and why now? We need a target for the arrows that is itself maximally stable — something with no moving parts to change. A concrete class has code that can churn; an interface has only names, so it barely ever changes. It is the perfect thing to depend on. This is the single load-bearing idea of DIP.

PICTURE. The green service now points at the blue interface. The interface sits in the middle, coloured blue for "stable contract."

Figure — SOLID — Dependency Inversion Principle

Step 5 — Make the detail point UP to the same contract

WHAT. We declare class MySqlOrderDb implements OrderRepository. The word implements creates a dependency arrow from the detail to the interface — the class promises to fulfil the contract.

WHY this is the "inversion." Compare with Step 2. There the red detail's arrow (well, the green→red arrow) pointed downward into the detail. Now the red detail's own arrow points upward, into the interface. The low-level module's arrow has been flipped from pointing at policy's mercy to pointing up at the shared contract. That literal reversal of one arrowhead is what the word "inversion" names.

PICTURE. Two arrows now converge on the blue interface from above (green) and below (red). Notice: nothing points down into the red box any more.

Figure — SOLID — Dependency Inversion Principle

Step 6 — Plug the detail in without naming it (Injection)

WHAT. One leak remains: if OrderService still writes new MySqlOrderDb() anywhere, its source code again names the red box, resurrecting the Step 2 arrow. So we forbid new. Instead the concrete object is handed in through the constructor:

class OrderService {
    private final OrderRepository repo;          // knows only the contract
    OrderService(OrderRepository repo){          // someone hands us a real one
        this.repo = repo;
    }
}

WHY. The service must use a real database at runtime but must not mention one at compile time. Passing the object in (this is Dependency Injection) keeps the runtime power while cutting the source-code arrow. The decision "which concrete?" moves out to an assembler at the program's edge (see Factory Pattern or Inversion of Control Containers).

PICTURE. A separate "Main / wiring" box (assembler) constructs the red MySqlOrderDb and pushes it into the green service. The service box contains no new — the volatile knowledge lives only at the edge.

Figure — SOLID — Dependency Inversion Principle

Step 7 — The payoff: add a new detail, edit nothing

WHAT. Suppose we now need an in-memory fake for tests, or a Postgres version. We write a brand-new leaf class implementing the same interface. We edit zero existing files; we only add one, then wire it in Main.

WHY this proves the principle worked. The measure of good dependency direction is: when a volatile thing changes, does the stable thing stay untouched? Here it does. The green box never opens. This is the quantitative promise from the parent's instability metric made visible: the service's efferent count toward details is now zero, so on that axis it is maximally stable.

PICTURE. Three red details (MySql, Postgres, FakeInMemory) all point up into the one blue contract; the green service, unchanged, points down into that same contract.

Figure — SOLID — Dependency Inversion Principle

Step 8 — The degenerate & edge cases (so nothing surprises you)

Every scenario, drawn once so you never meet an un-shown case:

Case A — one implementation, forever. Is the interface wasted? Almost. If a boundary is genuinely stable (never a DB, never a 3rd-party lib), skip it — the parent's Mistake 2 warns against wrapping everything. The indirection is a cost you pay only where change is likely.

Case B — the interface lives in the wrong package. If OrderRepository sits inside the MySql package, then deleting that package deletes the contract, so policy secretly depends on the detail package again. Fix: the interface lives with the policy. The stable package must own the contract, so the detail package depends on the policy package — the package-level inversion.

Case C — you inject a concrete. Covered in Step 6: DI present, DIP absent. The arrow re-appears the moment the parameter's type is a concrete class.

Case D — zero details wired. If Main forgets to inject, the constructor gets null and calls crash at runtime. The compiler is happy (contract satisfied) but the assembler failed. This is why wiring lives in one visible place — so a missing plug is easy to spot.

Figure — SOLID — Dependency Inversion Principle
Recall

Case A ::: One impl forever → the interface may be unnecessary indirection; only invert volatile boundaries. Case B ::: Interface in the detail package → policy still depends on the detail package; move the contract into the policy package. Case C ::: Injecting a concrete type → DI without DIP; the source-code arrow to the detail returns. Case D ::: No implementation injected → compiles fine, but null at runtime; keep wiring in one edge location.

The one-picture summary

This final frame compresses all eight steps: the before (green→red, value depends on churn) morphs into the after (green→blue←red, both depend on the stable contract; details injected from the edge).

Figure — SOLID — Dependency Inversion Principle
Recall Feynman: tell the whole walkthrough to a friend

"Picture two boxes and an arrow. The arrow means needs: the box it points at must already exist. At first my important business box, OrderService, points straight at the database box — so if the database changes, my precious logic breaks. Bad: the valuable thing needs the throwaway thing. So I slip a contract box between them — an interface, just a list of promised methods with no code, so it almost never changes. My logic now points at the contract. Then I make the database implement the contract, which flips its arrow so it points up at the contract too. Now both boxes point at the same stable middle — that flip of the database's arrow is the whole meaning of inversion. Last catch: my logic must never say new Database(), or the old bad arrow sneaks back; instead a wiring room at the program's edge builds the real database and hands it in. Payoff: to add a new database or a test fake, I write one new box and edit nothing that already works. And I only bother doing this where things actually change — not around every little value class."

Active Recall

What does the arrow mean on these diagrams?
's source code names ; delete and no longer compiles — " cannot exist without ."
Which single act is "the inversion"?
Making the low-level detail implements the abstraction, flipping its arrow to point UP at the contract instead of policy depending down on it.
Why forbid new Concrete() inside the high-level module?
new names the concrete in source code, resurrecting the direct policy→detail arrow; inject the abstraction from the edge instead.
Where must the abstraction live and why?
With the high-level policy package, so the detail package depends on the policy package — that is the package-level inversion.