2.2.8 · D5Design Principles

Question bank — SOLID — Dependency Inversion Principle

2,340 words11 min readBack to topic

Visual glossary — map the words to the code

Before the traps, pin down the vocabulary. Every phrase below maps to one concrete thing in code. Figure 1 shows the same four artifacts you will keep meeting.

Figure — SOLID — Dependency Inversion Principle
Figure — SOLID — Dependency Inversion Principle

The one formula this page assumes — instability

Several items below use Robert C. Martin's instability metric. Define it once, here, so no symbol appears unexplained.

Figure — SOLID — Dependency Inversion Principle

The refactor this whole page is about — before → after

Here is the transformation every trap orbits around: moving from a new-ed concrete to an injected interface. Figure 4 shows the dependency arrows flipping.

Figure — SOLID — Dependency Inversion Principle

How the neighbouring ideas plug into DIP

DIP does not live alone. Figure 5 places each related tool where it touches the inverted arrow.

Figure — SOLID — Dependency Inversion Principle

True or false — justify

DIP is satisfied the moment you pass an object into a constructor.
False — passing a concrete class in through a constructor is Dependency Injection but still couples policy to a detail; DIP needs the injected type to be an abstraction, not a concrete class.
"Inversion" means the low-level module now calls the high-level module.
False — inversion flips the source-code dependency arrow, not the runtime call direction; policy still calls the detail's method, but now both point at an interface the policy owns.
If a module has zero classes depending on it, it is maximally stable.
False — with the formula gives , which is maximally unstable; nothing depends on it, so it is free to change. Stability is .
Depending on an abstraction guarantees you never edit business logic again.
False — it protects you from implementation swaps; but if the contract itself must change (new method on the interface), you still edit policy. DIP shields against detail churn, not requirement churn.
An interface with only one implementation is a wasted abstraction.
False — if that implementation sits on a volatile boundary (DB, network, 3rd-party), the interface still buys testability and a swap-point; the value is in the seam, not the count of implementers.
Putting the interface in the same package as the database code satisfies DIP.
False — the abstraction must live with the high-level policy it serves, so the detail package depends up on the policy package; housing it with the DB re-couples policy to the detail's package.
DIP and the Open-Closed Principle are the same rule.
False but related — OCP is the outcome (add behaviour without editing existing code), DIP is one mechanism that achieves it by inverting the dependency onto a stable contract.
A FakeSender used only in tests still counts as a real second implementation justifying the interface.
True — testability is a first-class reason for the seam; being able to inject a fake instead of hitting real email is exactly the payoff DIP promises.

Spot the error

class Report { CsvExporter e = new CsvExporter(); } — this obeys DIP because CsvExporter is small and simple.
Error — size is irrelevant; new inside policy hard-wires a detail. If CsvExporter is a volatile export boundary it should sit behind an Exporter interface, injected, not new-ed.
"I made PaymentGateway an interface, but OrderService still does new StripeGateway() at the top and passes it to itself."
Error — self-new-ing the concrete keeps the source-code arrow pointing at the detail; the concrete must be supplied from outside (via Dependency Injection or a factory), or the inversion is only cosmetic.
"To follow DIP I wrapped every value class — Money, Date, Point — in its own interface."
Error — abstracting stable value types adds pure indirection cost with no swap benefit; DIP targets volatile boundaries (DB, UI, network), not simple stable data.
"The IEmailSender interface has a getSmtpPort() method so callers can configure the server."
Error — leaking smtp-specific detail into the contract means the abstraction now depends on a detail, violating clause 2; an SmsSender could never honour getSmtpPort().
"My high-level module imports com.app.mysql.MySqlRepo only in the type of a constructor parameter, so it's fine."
Error — that import is a source-code dependency on a detail; the parameter type must be the abstraction (Repository), with MySqlRepo unknown to the policy package.
"I used an IoC container, so DIP is automatically satisfied."
Error — a container is a delivery tool; it will happily wire a concrete into a concrete. DIP is a design goal about what types your policy references, independent of who does the wiring.

Why questions

Why must the abstraction be owned by the high-level module, not the low-level one?
So the dependency arrow genuinely inverts: the detail package points up at the policy package's contract. Ownership by policy is what makes it "inversion" rather than a normal downward dependency.
Why does DIP say to depend "toward stability"?
Because a dependency means "I break when you change." You want your valuable, rarely-changing policy to break rarely — so it must depend only on stable contracts, never on cheap, frequently-edited details.
Why isn't Dependency Injection enough on its own to give you DIP?
DI only decides how an object arrives; DIP decides what type the receiver references. Injecting a concrete satisfies DI but leaves the coupling — you need the injected type to be an abstraction.
Why does inverting the arrow make unit testing easier?
Because the policy references an interface, a test can inject a fake/mock implementation instead of the real DB or email, letting you test business logic in isolation with no I/O.
Why can adding a new implementation cost zero edits to existing files under DIP?
The policy already talks to the interface; a new class just implements it and gets injected. Nothing in the existing call graph names the new class, so nothing existing needs touching (the Open-Closed payoff).
Why does over-abstracting actually reduce flexibility in practice?
Every interface adds a layer of indirection to read and navigate; drowning stable code in needless contracts raises cognitive cost without any swap benefit, so the 80/20 gain evaporates.

Edge cases

If a module depends on nothing and nothing depends on it (an isolated leaf), what is its instability ?
Undefined — plugging and into gives , a division by zero; the metric simply does not apply to a fully disconnected module, so no direction is prescribed.
A "stable but concrete" utility (e.g. a standard string library) is depended on everywhere — does DIP demand you wrap it in an interface?
No — DIP targets volatile dependencies. A stable, non-changing library is safe to depend on concretely; wrapping it buys nothing and only adds indirection.
You have a truly single-implementation, never-changing internal detail. Should you still invert it?
Usually no — with no volatility and no test need, the abstraction is pure overhead. Invert only when change or testing pressure exists at that seam.
Two high-level modules both need the same seam — where does the shared interface live now?
In a neutral abstraction package both policies depend on, still above the details; it must never sink into a low-level package, or the inversion is lost. This is the shape Hexagonal Architecture / Ports and Adapters formalises as a "port."
Can you satisfy DIP with no Dependency Injection at all?
Yes — a factory or service locator that returns the abstraction also keeps policy referencing only the contract. DIP constrains the type referenced, not the delivery mechanism.
The interface exists, but the policy has a switch that news a specific concrete inside itself. Is DIP satisfied?
No — the concrete names still live inside policy, so the source-code coupling remains; push that construction choice out to a factory or the composition root.