Dependency inversion in depth
Module 03 showed dependency inversion as "inject the rules". This module separates the ideas people mix up: inversion (who owns the interface and which way arrows point), injection (how dependencies arrive), the composition root, containers, service locators and plugin architectures, with a booking service whose code never changes as its parts are swapped.
1. The idea
Dependency inversion has two halves. High-level policy shouldn't depend on low-level details; both should depend on abstractions. And the part people miss: the abstraction belongs to the high-level side. BookingService defines PaymentGateway in its own words; the card provider's adapter implements it. The arrow of dependency now points from the detail up to the policy, the opposite of the usual call direction. That reversal is the "inversion".
| Term | What it is | Relation to the principle |
|---|---|---|
| Dependency inversion | A design principle: who owns the interface, which way dependencies point | The goal |
| Dependency injection | A technique: give an object its collaborators (constructor, method or setter) instead of letting it create them | The usual way to achieve it |
| Composition root | The one place (often main) where concrete classes are chosen and wired | Where all the "new"s live |
| IoC container | A library that builds the object graph from registrations (Spring, .NET's built-in container, Guice, NestJS) | Automates the composition root in large apps |
| Service locator | Objects ask a global registry for what they need at run time | Inverts too, but hides dependencies inside methods; most teams prefer injection |
| Plugin architecture | The core defines interfaces; plugins implementing them are found and loaded at start-up | The same principle at the size of a whole program |
2. How it works
Watch which way the arrows point when BookingService creates its own dependencies, and when it owns interfaces that details implement.
3. Try it yourself
Swap the parts
Choose what the composition root plugs in, then book seat A7 for Mia. Notice what changes in the wiring, and what never changes.
4. In code
A booking service that owns three small interfaces (payments, time, notifications). Production and a test wire different parts into the same class: the test declines the payment and checks that the seat is released and nothing is sent.
from __future__ import annotations
from abc import ABC, abstractmethod
# ---- Owned by the booking logic: the interfaces it needs, in its own words. ----
class PaymentGateway(ABC):
@abstractmethod
def charge(self, customer: str, cents: int) -> bool: ...
class Clock(ABC):
@abstractmethod
def now(self) -> str: ...
class Notifier(ABC):
@abstractmethod
def send(self, customer: str, text: str) -> None: ...
class BookingService:
"""High-level policy. It names no payment provider, no real clock, no mail server."""
def __init__(self, payments: PaymentGateway, clock: Clock, notifier: Notifier) -> None:
self.payments, self.clock, self.notifier = payments, clock, notifier
self.taken: set[str] = set()
def book(self, seat: str, customer: str, cents: int) -> bool:
if seat in self.taken:
return False
self.taken.add(seat) # hold the seat while paying
if not self.payments.charge(customer, cents):
self.taken.discard(seat) # declined: release it
return False
self.notifier.send(customer, f"seat {seat} confirmed at {self.clock.now()}")
return True
# ---- Details, plugged in from outside. ----
class ApprovingGateway(PaymentGateway): # stands in for a real card provider's client
def charge(self, customer: str, cents: int) -> bool:
return True
class DecliningGateway(PaymentGateway):
def charge(self, customer: str, cents: int) -> bool:
return False
class FixedClock(Clock):
def __init__(self, time: str) -> None:
self.time = time
def now(self) -> str:
return self.time
class PrintingNotifier(Notifier):
def send(self, customer: str, text: str) -> None:
print(f"to {customer}: {text}")
class RecordingNotifier(Notifier):
def __init__(self) -> None:
self.sent: list[str] = []
def send(self, customer: str, text: str) -> None:
self.sent.append(text)
if __name__ == "__main__":
# Composition root: the only place that knows which concrete classes are used.
live = BookingService(ApprovingGateway(), FixedClock("18:00"), PrintingNotifier())
print("A7 booked:", live.book("A7", "Mia", 1200))
print("A7 again:", live.book("A7", "Leo", 1200))
# A test wires different parts into the same, unchanged BookingService.
outbox = RecordingNotifier()
test = BookingService(DecliningGateway(), FixedClock("09:00"), outbox)
print("A8 booked:", test.book("A8", "Mia", 1200), "| A8 free again:", "A8" not in test.taken, "| messages:", len(outbox.sent))
// ---- Owned by the booking logic: the interfaces it needs, in its own words. ----
interface PaymentGateway { charge(customer: string, cents: number): boolean; }
interface Clock { now(): string; }
interface Notifier { send(customer: string, text: string): void; }
// High-level policy. It names no payment provider, no real clock, no mail server.
class BookingService {
readonly taken = new Set<string>();
constructor(private payments: PaymentGateway, private clock: Clock, private notifier: Notifier) {}
book(seat: string, customer: string, cents: number): boolean {
if (this.taken.has(seat)) return false;
this.taken.add(seat); // hold the seat while paying
if (!this.payments.charge(customer, cents)) { this.taken.delete(seat); return false; } // declined: release it
this.notifier.send(customer, `seat ${seat} confirmed at ${this.clock.now()}`);
return true;
}
}
// ---- Details, plugged in from outside. ----
class ApprovingGateway implements PaymentGateway { charge(): boolean { return true; } } // stands in for a real card provider's client
class DecliningGateway implements PaymentGateway { charge(): boolean { return false; } }
class FixedClock implements Clock { constructor(private time: string) {} now(): string { return this.time; } }
class PrintingNotifier implements Notifier { send(customer: string, text: string): void { console.log(`to ${customer}: ${text}`); } }
class RecordingNotifier implements Notifier { readonly sent: string[] = []; send(_c: string, text: string): void { this.sent.push(text); } }
const py = (b: boolean): string => (b ? "True" : "False");
// Composition root: the only place that knows which concrete classes are used.
const live = new BookingService(new ApprovingGateway(), new FixedClock("18:00"), new PrintingNotifier());
console.log("A7 booked: " + py(live.book("A7", "Mia", 1200)));
console.log("A7 again: " + py(live.book("A7", "Leo", 1200)));
// A test wires different parts into the same, unchanged BookingService.
const outbox = new RecordingNotifier();
const test = new BookingService(new DecliningGateway(), new FixedClock("09:00"), outbox);
console.log(`A8 booked: ${py(test.book("A8", "Mia", 1200))} | A8 free again: ${py(!test.taken.has("A8"))} | messages: ${outbox.sent.length}`);import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
// ---- Owned by the booking logic: the interfaces it needs, in its own words. ----
interface PaymentGateway { boolean charge(String customer, int cents); }
interface Clock { String now(); }
interface Notifier { void send(String customer, String text); }
// High-level policy. It names no payment provider, no real clock, no mail server.
class BookingService {
final Set<String> taken = new HashSet<>();
private final PaymentGateway payments;
private final Clock clock;
private final Notifier notifier;
BookingService(PaymentGateway payments, Clock clock, Notifier notifier) { this.payments = payments; this.clock = clock; this.notifier = notifier; }
boolean book(String seat, String customer, int cents) {
if (taken.contains(seat)) return false;
taken.add(seat); // hold the seat while paying
if (!payments.charge(customer, cents)) { taken.remove(seat); return false; } // declined: release it
notifier.send(customer, "seat " + seat + " confirmed at " + clock.now());
return true;
}
}
// ---- Details, plugged in from outside. ----
class ApprovingGateway implements PaymentGateway { public boolean charge(String c, int cents) { return true; } } // stands in for a real card provider's client
class DecliningGateway implements PaymentGateway { public boolean charge(String c, int cents) { return false; } }
class FixedClock implements Clock { private final String time; FixedClock(String time) { this.time = time; } public String now() { return time; } }
class PrintingNotifier implements Notifier { public void send(String customer, String text) { System.out.println("to " + customer + ": " + text); } }
class RecordingNotifier implements Notifier { final List<String> sent = new ArrayList<>(); public void send(String c, String text) { sent.add(text); } }
public class Main {
static String py(boolean b) { return b ? "True" : "False"; }
public static void main(String[] args) {
// Composition root: the only place that knows which concrete classes are used.
BookingService live = new BookingService(new ApprovingGateway(), new FixedClock("18:00"), new PrintingNotifier());
System.out.println("A7 booked: " + py(live.book("A7", "Mia", 1200)));
System.out.println("A7 again: " + py(live.book("A7", "Leo", 1200)));
// A test wires different parts into the same, unchanged BookingService.
RecordingNotifier outbox = new RecordingNotifier();
BookingService test = new BookingService(new DecliningGateway(), new FixedClock("09:00"), outbox);
System.out.println("A8 booked: " + py(test.book("A8", "Mia", 1200)) + " | A8 free again: " + py(!test.taken.contains("A8")) + " | messages: " + outbox.sent.size());
}
}Output (the same in all three languages)
to Mia: seat A7 confirmed at 18:00 A7 booked: True A7 again: False A8 booked: False | A8 free again: True | messages: 0
5. In the interview
- Inject through the constructor and say why: "BookingService takes a PaymentGateway, so we can swap providers and test it with a fake."
- Put interfaces next to the code that uses them.
PaymentGatewaylives in the booking package, shaped by booking's needs, not copied from the provider's SDK. - Inject time and randomness too. A
Clockmakes hold expiry testable; a seeded random source makes simulations repeatable. - Don't invert everything. Value objects, the standard library and pure helpers don't need interfaces. Invert at boundaries: I/O, external services, time, and things that vary.
- Know the vocabulary: inversion is the principle, injection the technique, a container a tool for it, a service locator the alternative most teams avoid.
6. Remember it
How this hook works: what “Policy owns the plug” means
What the slogan means, phrase by phrase
- “Policy”
- The business rules: booking seats, holding them, releasing them.
- “owns the plug”
- It defines PaymentGateway, Clock and Notifier; adapters fit them.
Why this picture fits: The box office writes down what it needs from a bank; any bank that fits is plugged in.
The story → the concept
| The cinema | The code |
|---|---|
| Box office | BookingService |
| "Take a payment" | PaymentGateway, owned by booking |
| The bank's terminal | A card provider adapter |
| Changing banks | One line in the composition root |
Watch it on an example
- Production plugs in the card provider, system clock and email.
- Tests plug in a declining gateway, a fixed clock and a recording notifier.
- BookingService's code is identical in both.
The trap it saves you from
Calling new StripeClient() (or any concrete detail) inside business logic, so it can't be swapped or tested.
It still works when…
With a container: the container just writes the composition root for you; the principle is the same.
7. Common mistakes
8. Practice exercise (25 minutes)
- Find the news (5 min). In your last LLD solution, list every place business logic creates a concrete dependency.
- Own the interface (10 min). Define the interface in the business code's words, and move the creation to main.
- Inject time (5 min). Replace any direct clock call with an injected Clock and write a test at a fixed time.
- Draw the arrows (5 min). Sketch the packages: do all dependency arrows point towards the business rules?
9. Self-check quiz
What is the difference between dependency inversion and dependency injection?
Where should the PaymentGateway interface live?
Why do many teams avoid the service locator pattern?
What's the composition root?
10. Go deeper
| Resource | Time | Why |
|---|---|---|
| Module 03: SOLID in the interview | Next | All five on one class, and the questions interviewers ask. |
| Inside a service: hexagonal and clean architecture (Software architecture) | 30 min | Dependency inversion as the rule behind ports and adapters. |
| P12: Movie ticket booking | 45 min | The full problem this module's example comes from. |
| Inversion of Control Containers and the Dependency Injection pattern (Martin Fowler) | 20 min | Where "dependency injection" and "service locator" were named and compared. |