2.2.8 · D3Design Principles

Worked examples — SOLID — Dependency Inversion Principle

3,933 words18 min readBack to topic

This is a worked-example child of SOLID — Dependency Inversion Principle (DIP). The parent built the idea; here we drill every case the principle can throw at you — from the textbook "insert an interface" case, all the way to the degenerate cases (no interface, wrong interface owner, an interface that hides nothing) and the numeric-metric cases.

Nothing here assumes you memorised the parent. Every term, symbol and heuristic is re-earned before use — including the coupling counts and the three metrics, which we define now, before the matrix uses them.


First: the four counting words and three metrics

The matrix below and Examples 6–7 measure dependencies with numbers. Those numbers rest on four plain-English counting ideas and three ratios. We define all seven here so no symbol appears before its meaning.

With , , , , now defined, the matrix can safely use them.


The scenario matrix

Unlike geometry there are no quadrants, but there is a finite set of shapes a dependency arrow can take, plus the numeric metrics just defined. Here is the full map:

Cell Case class What makes it tricky
A The canonical fix high-level → concrete, insert interface
B Injection channel varies constructor vs setter vs factory delivery
C Degenerate: DI without inversion you inject, but inject a concrete type
D Degenerate: interface owned by the wrong side arrow looks inverted but isn't
E Degenerate: pointless abstraction an interface with exactly one forever-user
F Zero-dependency module : what is its instability ?
G Zero-dependent module : what is its instability ?
H Boundary limiting value the main sequence , and deviation from it
I Real-world word problem payment gateways, swap at runtime
J Exam twist "does this actually violate DIP?" trick

Every example below is tagged with the cell(s) it covers. Examples 1–9 hit all ten cells (Example 6 covers F and G together).


Example 1 — The canonical fix [Cell A]

Step 1 — Draw the current arrow. ReportServicePdfWriter (concrete).

Why this step? Before you can invert an arrow you must see which way it points. Look at the left half of the figure below: the two black boxes are stacked, ReportService on top and PdfWriter on the bottom, and a single black arrow runs straight down from policy into the concrete detail. Under it the caption reads "BEFORE" and "policy depends on detail." That downward black arrow is the whole problem.

Figure — SOLID — Dependency Inversion Principle
Figure s01 — Left ("BEFORE"): two stacked black boxes, ReportService above PdfWriter, one black arrow pointing down from policy into the concrete detail. Right ("AFTER"): three stacked boxes, ReportService on top, a red DocumentWriter interface in the middle, PdfWriter at the bottom; two red arrows both point at the middle red interface (policy points down into it, concrete points up into it).

Step 2 — Name the stable side and the volatile side. ReportService = policy (stable, valuable). PdfWriter = mechanism (volatile — tomorrow it's HtmlWriter).

Why this step? DIP's rule is "arrow points toward stability." You cannot apply that rule until you have labelled which box is stable. Here the stable one is ReportService.

Step 3 — Introduce an abstraction owned by the policy. Define interface DocumentWriter { void write(Report r); }, placed in the policy package. Make PdfWriter implements DocumentWriter.

Why this step? An interface is a contract with no implementation — a promised method list. By making the policy depend on the promise instead of the thing, the concrete's arrow flips up toward the promise. Now look at the right half of the figure: three boxes stacked — ReportService on top, the red DocumentWriter interface in the middle, and PdfWriter at the bottom. The top arrow runs down from ReportService into the red box, and the bottom arrow runs up from PdfWriter into the same red box. Both arrows are red and both point at the interface — that is the inversion made visible.

Step 4 — Inject the concrete. ReportService(DocumentWriter w) — the concrete arrives through the constructor, never via new inside.

Why this step? If ReportService still said new PdfWriter(), the source-code dependency would sneak straight back in — the runtime object may be abstract but the compile-time text still names the concrete. Injection keeps the policy file free of any concrete name.

Verify. The point that is not obvious from the forecast: swapping to HtmlWriter now edits zero existing files — you add one leaf class implementing DocumentWriter and change nothing in policy. The downward arrow count from ReportService to a concrete is 0, confirming the arrow now lands only on the interface. See Dependency Injection for the delivery mechanism.


Example 2 — Same fix, three delivery channels [Cell B]

Step 1 — Constructor injection. new ReportService(new PdfWriter()). Why this step? Passing in the constructor lets the w field be final — set exactly once, never reassigned, never null. This is the default; prefer it.

Step 2 — Setter injection. service.setWriter(new PdfWriter()). Why this step? Some frameworks build the object first, wire later. The cost: the w field can be reassigned or left null, so the reference is re-bindable after construction.

Step 3 — Factory / locator. service = ReportFactory.build() where the factory picks the writer and calls the constructor for you. Why this step? Shows DIP ≠ DI: a factory or an IoC container can satisfy DIP without classic hand-injection. Note carefully: the factory only decides which writer to pass; whether the resulting w reference is final depends on how the factory constructs the object. If the factory uses the constructor of Step 1, the reference is final; a factory does not by itself guarantee an immutable object or immutable dependencies.

Verify. The non-obvious catch: the factory does not have its own mutability story — it inherits whatever the constructor it calls provides. Only the setter exposes a mutator that re-binds w after construction; constructor injection makes w final. Forecast answer: the setter.


Example 3 — DI but NOT inversion (degenerate) [Cell C]

Step 1 — Look at the type in the constructor signature. It is PdfWriter, a concrete class, not an interface.

Why this step? DIP is about what the type names, not about who calls new. The compile-time dependency is ReportService → PdfWriter regardless of who constructs it.

Step 2 — Check whether the arrow points at an abstraction. It points at a concrete. Arrow still runs policy → detail. Not inverted.

Why this step? This is the classic "DI is a technique, DIP is a goal" gap: you can perform the injection technique and still miss the abstraction goal, because the injected type is concrete.

Verify. The insight beyond the forecast: it is the type in the signature, not the presence of injection, that decides DIP. Swapping to HtmlWriter forces an edit to the ReportService constructor signature — a change inside policy — so the whole payoff of inversion is absent. Claim is false.


Example 4 — Interface owned by the wrong side (degenerate) [Cell D]

Step 1 — Trace the package-level arrow. report-policy imports DocumentWriter from pdf-detail. So the package arrow points policy → detail.

Why this step? An interface only inverts the arrow if it sits on the stable side. The abstraction must live with the policy — otherwise the policy package still cannot compile without the detail package. Look at the left ("WRONG") half of the figure below: a dashed black box labelled "pdf-detail package" encloses the red DocumentWriter interface together with PdfWriter, and the black report-policy box sits outside it with an arrow reaching down and in — caption "policy → detail (NOT inverted)."

Figure — SOLID — Dependency Inversion Principle
Figure s02 — Left ("WRONG"): a dashed package box "pdf-detail" contains both the red DocumentWriter interface and PdfWriter; report-policy sits outside with an arrow pointing down into that package (policy → detail). Right ("RIGHT"): a dashed package box "report-policy" contains ReportService and the red DocumentWriter interface; PdfWriter sits outside and below with an arrow pointing up into the interface (detail → policy). The red interface box has moved into the policy package.

Step 2 — Compare with the correct ownership. Move DocumentWriter into report-policy. Now pdf-detail imports it from policy: package arrow detail → policy. Inverted!

Why this step? True inversion is visible at the package level: the detail package must depend on the policy package, never the reverse. On the right ("RIGHT") half of the figure, the dashed box labelled "report-policy package" now encloses both ReportService and the red DocumentWriter interface, while PdfWriter sits outside and below, with a red arrow reaching up into the interface. The red interface box has physically moved into the policy package; that relocation is the entire fix.

Verify. A compile-time test settles it: with the interface in pdf-detail, deleting pdf-detail breaks report-policy (proof policy still depends on detail). With it in report-policy, deleting pdf-detail leaves policy compiling fine. Only the second is truly inverted. The interface here is the port; see Coupling and Cohesion and Hexagonal Architecture / Ports and Adapters.


Example 5 — Pointless abstraction (over-application) [Cell E]

Step 1 — Estimate the number of implementers over the software's life. Money is a stable value type. Expected implementers: exactly one, forever.

Why this step? An abstraction pays off only where the concrete is likely to be swapped — at volatile boundaries (database, network, UI, 3rd-party libraries). An interface with one permanent implementer buys zero swappability while still adding a layer of indirection to navigate.

Step 2 — Apply the effort-vs-payoff (Pareto) reasoning. The 80/20 rule (the Pareto principle) is the observation that roughly 80% of the benefit comes from 20% of the effort. Applied here: invert only the few seams likely to change; leave the many stable value classes concrete. Wrapping every class inverts effort into the low-payoff 80%.

Why this step? More interfaces look flexible but add navigation cost with no future payoff. Concentrate abstraction where change actually happens.

Verify. Model the payoff as (number of future swaps) × (cost saved per swap). For Money, future swaps ≈ 0, so payoff ≈ 0 while indirection cost > 0 → net negative. Don't wrap it. Contrast with SOLID — Interface Segregation Principle, which likewise resists needless interface bloat.


Example 6 — Instability at the two coupling extremes [Cell F, G]

Step 1 — Plug the interface into the formula (the extreme, Cell F). (five classes point at it), (it points at nothing).

Why this step? means maximally stable — nothing it depends on can force it to change. The interface should be stable; many things lean on it. This is the correct DIP shape.

Step 2 — Plug in Main (the extreme, Cell G). , .

Why this step? means maximally unstable — free to change because nothing depends on it. A top-level entry point should be unstable; that's where volatile wiring lives.

Step 3 — Read the direction rule. Dependencies should flow from high- (unstable) toward low- (stable): Main () → DocumentWriter (). Arrow points toward stability. ✔

Why this step? This is the quantified version of "invert the arrow toward stability."

Verify. The non-obvious payoff: these two computations bracket the whole scale — every real module lands between the interface () and the entry point (). The interface is the one, and arrows correctly run unstable → stable. ✔


Example 7 — The main sequence limiting line [Cell H, limiting]

Step 1 — Compute .

Why this step? near 1 means "almost all contracts, little implementation" — appropriate only for a stable package (low ).

Step 2 — Compute the deviation distance .

Why this step? balances abstractness against stability: a stable package ( low) should be abstract ( high) so implementers can depend on it — exactly DIP. measures how far the package strays from that ideal line; is dead-on.

Step 3 — Interpret . is tiny → the package is stable and abstract, sitting almost exactly on the main sequence: a well-formed DIP boundary. It is not in the "zone of pain" (low , low — rigid concrete that everything depends on yet cannot change) nor the "zone of uselessness" (high , high — abstract but unused).

Why this step? Those two zones are the degenerate limits of the metric: (concrete + stable = can't change, hurts) and (abstract + unstable = nobody uses it). Knowing them tells you which direction to correct a bad package.

Verify. Beyond the arithmetic: because is high and is low, abstractness matches stability — so the answer to the forecast is "neither too abstract nor too concrete; it is correctly balanced," which is exactly what the small certifies. ✔


Example 8 — Real-world word problem: swappable payment gateway [Cell I]

Step 1 — Identify policy vs volatile detail. Policy = CheckoutService ("charge total, then confirm order"). Volatile detail = the gateway (Stripe/Razorpay — external 3rd party, guaranteed to change).

Why this step? Gateways are the textbook volatile boundary where DIP pays off — exactly the "invert the few seams that change" rule from Example 5.

Step 2 — Define the port, owned by policy. interface PaymentGateway { Receipt charge(Money m); } in the checkout package.

Why this step? This is the socket idea: the checkout (policy) and Stripe (detail) both agree on the plug (PaymentGateway) and neither names the other directly. Interchangeable implementations behind one interface is the Strategy Pattern.

Step 3 — Implement the adapters. Write StripeGateway implements PaymentGateway today; later add RazorpayGateway implements PaymentGateway; for tests write FakeGateway implements PaymentGateway that returns a canned Receipt and charges nobody. Each adapter's arrow points up to PaymentGateway; none is named inside CheckoutService.

Why this step? Tests must never hit a real card, so the fake is essential — this is DIP's concrete testing benefit.

Step 4 — Inject at the composition root. new CheckoutService(new StripeGateway(...)) in Main; to go live with Razorpay, change that one wiring line to new RazorpayGateway(...).

Why this step? The composition root is the only place that names a concrete gateway, so it is the only place a swap touches.

Verify. The non-obvious accounting: adding Razorpay creates one new leaf class and edits one wiring line at the composition root, for zero existing core-logic files touched. Without DIP you would edit CheckoutService itself and risk breaking live payments — that risk delta is the real payoff. Forecast answer: 0 core files.


Example 9 — Exam twist: does this actually violate DIP? [Cell J]

Step 1 — Check volatility of the depended-on class. Clock looks stable, but it has one volatile face: testing time-dependent logic. Verifying "token expires after 60s" against a real wall clock means waiting 60 real seconds.

Why this step? DIP targets volatile dependencies. A dependency's badness comes from whether it will change or block testing, not merely from being concrete.

Step 2 — Judge (a). new Clock() welds real wall-clock time inside TokenService. The expiry test becomes impossible to run quickly. Violation with real cost.

Step 3 — Judge (b). Still a concrete Clock type in the signature → by the Example 3 rule, not truly inverted; but at least injectable, so a subclass could fake time awkwardly. The clean fix: introduce interface TimeSource { long now(); } and inject a FixedTime in tests.

Why this step? The twist mirrors Example 3's trap: the injected version looks safer yet still names a concrete type, so it is only partly fixed.

Verify. The lesson beyond "a or b": both benefit from a TimeSource interface, but (a) is the worse violation because it cannot be tested fast at all, while (b) is testable-but-ugly. The genuinely DIP-clean design depends on the abstraction TimeSource. ✔


Recall

Recall One-line drills

Define afferent coupling in words. ::: The number of classes outside a module that point at (depend on) it — arrows arriving. Define efferent coupling in words. ::: The number of classes a module points at (depends on) — arrows exiting. Cell A — after inserting the interface, how many concrete classes does policy name? ::: Zero. Cell C — injecting a concrete type: DIP satisfied? ::: No, the arrow still points at a detail. Cell D — where must the interface live to truly invert the package arrow? ::: In the high-level policy package. Cell E — should you wrap a stable single-implementer value class in an interface? ::: No — payoff ≈ 0, indirection cost > 0. Cell F — a module with has instability ? ::: (maximally stable). Cell G — a module with has instability ? ::: (maximally unstable). Cell H — the ideal line and the deviation distance are? ::: , and . Cell I — existing core files edited to add a new gateway? ::: Zero (add a leaf + one wiring line). Cell J — is an injected but concrete-typed Clock truly inverted? ::: No — needs a TimeSource interface.


Parent: SOLID — Dependency Inversion Principle (DIP) · siblings SOLID — Single Responsibility Principle, SOLID — Open-Closed Principle, SOLID — Liskov Substitution Principle.