Dependency injection
Dependency injection means a class receives what it depends on instead of creating it, so one place decides the real wiring and tests can pass predictable fakes. This page injects Pedal's payment gateway, clock and notifier into the rental service, and tests the late-night rule without waiting for 10 p.m.
1. The pain
Pedal's RentalService starts a rental: it charges the unlock fee (free after 10 p.m.), then sends a push notification. The first version created what it needed inside itself: self.payments = LumenGateway(), datetime.now() for the time, PushNotifier() for messages.
Tests couldn't swap any of it. Every test run charged the payment provider's sandbox and sent real pushes to a test phone; the "free after 10 p.m." test passed only when the suite happened to run late at night. And nothing in the class's signature said it depended on three outside systems.
2. The idea in one picture
Dependency injection means an object receives the things it depends on from outside, usually through its constructor, instead of creating or finding them itself. RentalService(payments, clock, notifier) asks for three interfaces. One place in the program, the composition root, decides which real implementations to pass in production; tests pass predictable fakes.
It's the practical side of the dependency inversion principle: depend on interfaces you're given, not on concrete classes you build. A DI container (Spring, .NET's built-in one, NestJS) automates the wiring for large apps, but the pattern needs no framework; this page wires everything by hand.
3. The roles
| Role | Its job | In Pedal |
|---|---|---|
| Service (client) | Declares what it needs, usually as constructor parameters; creates none of it. | RentalService(payments, clock, notifier) |
| Dependency interfaces | What the service relies on. | PaymentGateway, Clock, Notifier |
| Implementations | Real ones for production, doubles for tests. | LumenGateway, WallClock, PushNotifier; FakeGateway, FixedClock, RecordingNotifier |
| Composition root (injector) | The one place that builds the real object graph. | build_production() |
4. Before and after
The rental service both ways (Python sketch; the full program is in section 5).
Without the pattern
class RentalService:
def __init__(self):
self.payments = LumenGateway() # real money
self.notifier = PushNotifier() # real phones
def start_rental(self, rider):
hour = datetime.now().hour # real time
fee = 0 if hour >= 22 else 100
...With it
class RentalService:
def __init__(self, payments, clock, notifier):
self.payments = payments
self.clock = clock
self.notifier = notifier
# production, in one place:
RentalService(LumenGateway(), WallClock(), PushNotifier())
# a test:
RentalService(FakeGateway(), FixedClock(23), RecordingNotifier())| The next change | Without the pattern | With it |
|---|---|---|
| Test the late-night rule | Wait until 10 p.m., or patch the time module | Pass FixedClock(23) |
| Switch payment provider | Edit RentalService | Change one line in the composition root |
| See what the service depends on | Read its whole body | Read its constructor |
The service decides what it needs; the composition root decides what it gets.
5. The code
RentalService takes three interfaces. build_production() wires the real ones; the test wires fakes and checks the late-night rule with a fixed clock.
from __future__ import annotations
from abc import ABC, abstractmethod
import datetime
# ---- The dependencies, as interfaces. ----
class PaymentGateway(ABC):
@abstractmethod
def charge(self, rider: str, pence: int) -> str: ...
class Clock(ABC):
@abstractmethod
def hour(self) -> int: ...
class Notifier(ABC):
@abstractmethod
def send(self, rider: str, text: str) -> None: ...
# ---- The service: asks for what it needs in its constructor, and creates none of it. ----
class RentalService:
def __init__(self, payments: PaymentGateway, clock: Clock, notifier: Notifier) -> None:
self.payments, self.clock, self.notifier = payments, clock, notifier
def start_rental(self, rider: str) -> str:
fee = 0 if self.clock.hour() >= 22 else 100 # late-night unlocks are free
ref = self.payments.charge(rider, fee)
self.notifier.send(rider, "ride started")
return f"{rider}: unlock fee {fee}p, payment {ref}"
def wiring(self) -> str:
return f"RentalService({type(self.payments).__name__}, {type(self.clock).__name__}, {type(self.notifier).__name__})"
# ---- Real implementations, used in production. ----
class LumenGateway(PaymentGateway):
def charge(self, rider: str, pence: int) -> str:
raise RuntimeError("would call the real payment provider")
class WallClock(Clock):
def hour(self) -> int:
return datetime.datetime.now().hour
class PushNotifier(Notifier):
def send(self, rider: str, text: str) -> None:
raise RuntimeError("would send a real phone notification")
# ---- Test doubles: predictable and harmless. ----
class FakeGateway(PaymentGateway):
def __init__(self) -> None:
self.charges: list[int] = []
def charge(self, rider: str, pence: int) -> str:
self.charges.append(pence)
return f"FAKE-{len(self.charges)}"
class FixedClock(Clock):
def __init__(self, hour: int) -> None:
self.at = hour
def hour(self) -> int:
return self.at
class RecordingNotifier(Notifier):
def __init__(self) -> None:
self.sent: list[str] = []
def send(self, rider: str, text: str) -> None:
self.sent.append(f"{rider}: {text}")
def build_production() -> RentalService:
"""The composition root: the one place that chooses real implementations."""
return RentalService(LumenGateway(), WallClock(), PushNotifier())
if __name__ == "__main__":
print(f"production wiring: {build_production().wiring()}")
gateway, clock, notifier = FakeGateway(), FixedClock(14), RecordingNotifier()
service = RentalService(gateway, clock, notifier)
print(f"test at 14:00 -> {service.start_rental('mia')}")
clock.at = 23
print(f"test at 23:00 -> {service.start_rental('leo')}")
print(f"fake gateway charged: {gateway.charges} | messages: {'; '.join(notifier.sent)}")
print(f"test wiring: {service.wiring()}")
// ---- The dependencies, as interfaces. ----
interface PaymentGateway { charge(rider: string, pence: number): string; }
interface Clock { hour(): number; }
interface Notifier { send(rider: string, text: string): void; }
// ---- The service: asks for what it needs in its constructor, and creates none of it. ----
class RentalService {
constructor(private readonly payments: PaymentGateway, private readonly clock: Clock, private readonly notifier: Notifier) {}
startRental(rider: string): string {
const fee = this.clock.hour() >= 22 ? 0 : 100; // late-night unlocks are free
const ref = this.payments.charge(rider, fee);
this.notifier.send(rider, "ride started");
return `${rider}: unlock fee ${fee}p, payment ${ref}`;
}
wiring(): string {
return `RentalService(${this.payments.constructor.name}, ${this.clock.constructor.name}, ${this.notifier.constructor.name})`;
}
}
// ---- Real implementations, used in production. ----
class LumenGateway implements PaymentGateway {
charge(): string { throw new Error("would call the real payment provider"); }
}
class WallClock implements Clock {
hour(): number { return new Date().getHours(); }
}
class PushNotifier implements Notifier {
send(): void { throw new Error("would send a real phone notification"); }
}
// ---- Test doubles: predictable and harmless. ----
class FakeGateway implements PaymentGateway {
readonly charges: number[] = [];
charge(rider: string, pence: number): string { void rider; this.charges.push(pence); return `FAKE-${this.charges.length}`; }
}
class FixedClock implements Clock {
constructor(public at: number) {}
hour(): number { return this.at; }
}
class RecordingNotifier implements Notifier {
readonly sent: string[] = [];
send(rider: string, text: string): void { this.sent.push(`${rider}: ${text}`); }
}
// The composition root: the one place that chooses real implementations.
const buildProduction = (): RentalService => new RentalService(new LumenGateway(), new WallClock(), new PushNotifier());
console.log(`production wiring: ${buildProduction().wiring()}`);
const gateway = new FakeGateway(), clock = new FixedClock(14), notifier = new RecordingNotifier();
const service = new RentalService(gateway, clock, notifier);
console.log(`test at 14:00 -> ${service.startRental("mia")}`);
clock.at = 23;
console.log(`test at 23:00 -> ${service.startRental("leo")}`);
console.log(`fake gateway charged: [${gateway.charges.join(", ")}] | messages: ${notifier.sent.join("; ")}`);
console.log(`test wiring: ${service.wiring()}`);import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
public class Main {
// ---- The dependencies, as interfaces. ----
interface PaymentGateway { String charge(String rider, int pence); }
interface Clock { int hour(); }
interface Notifier { void send(String rider, String text); }
// ---- The service: asks for what it needs in its constructor, and creates none of it. ----
static class RentalService {
private final PaymentGateway payments;
private final Clock clock;
private final Notifier notifier;
RentalService(PaymentGateway payments, Clock clock, Notifier notifier) { this.payments = payments; this.clock = clock; this.notifier = notifier; }
String startRental(String rider) {
int fee = clock.hour() >= 22 ? 0 : 100; // late-night unlocks are free
String ref = payments.charge(rider, fee);
notifier.send(rider, "ride started");
return rider + ": unlock fee " + fee + "p, payment " + ref;
}
String wiring() {
return "RentalService(" + payments.getClass().getSimpleName() + ", " + clock.getClass().getSimpleName() + ", " + notifier.getClass().getSimpleName() + ")";
}
}
// ---- Real implementations, used in production. ----
static class LumenGateway implements PaymentGateway {
public String charge(String rider, int pence) { throw new UnsupportedOperationException("would call the real payment provider"); }
}
static class WallClock implements Clock {
public int hour() { return LocalTime.now().getHour(); }
}
static class PushNotifier implements Notifier {
public void send(String rider, String text) { throw new UnsupportedOperationException("would send a real phone notification"); }
}
// ---- Test doubles: predictable and harmless. ----
static class FakeGateway implements PaymentGateway {
final List<Integer> charges = new ArrayList<>();
public String charge(String rider, int pence) { charges.add(pence); return "FAKE-" + charges.size(); }
}
static class FixedClock implements Clock {
int at;
FixedClock(int at) { this.at = at; }
public int hour() { return at; }
}
static class RecordingNotifier implements Notifier {
final List<String> sent = new ArrayList<>();
public void send(String rider, String text) { sent.add(rider + ": " + text); }
}
// The composition root: the one place that chooses real implementations.
static RentalService buildProduction() { return new RentalService(new LumenGateway(), new WallClock(), new PushNotifier()); }
public static void main(String[] args) {
System.out.println("production wiring: " + buildProduction().wiring());
FakeGateway gateway = new FakeGateway();
FixedClock clock = new FixedClock(14);
RecordingNotifier notifier = new RecordingNotifier();
RentalService service = new RentalService(gateway, clock, notifier);
System.out.println("test at 14:00 -> " + service.startRental("mia"));
clock.at = 23;
System.out.println("test at 23:00 -> " + service.startRental("leo"));
System.out.println("fake gateway charged: " + gateway.charges + " | messages: " + String.join("; ", notifier.sent));
System.out.println("test wiring: " + service.wiring());
}
}Output (the same in all three languages)
production wiring: RentalService(LumenGateway, WallClock, PushNotifier) test at 14:00 -> mia: unlock fee 100p, payment FAKE-1 test at 23:00 -> leo: unlock fee 0p, payment FAKE-2 fake gateway charged: [100, 0] | messages: mia: ride started; leo: ride started test wiring: RentalService(FakeGateway, FixedClock, RecordingNotifier)
6. Try it
Wire the service
Choose where the code is running and what to inject, then start a rental for Mia.
7. When not to use it
What you get
- Testable: fakes for anything slow, costly or unpredictable.
- Honest signatures: the constructor lists every dependency.
- Swappable: change an implementation in one place.
- No hidden globals: nothing reaches out for a singleton.
What you pay
- More parameters: big constructors are a sign the class does too much.
- Wiring code: someone has to build the object graph.
- Containers add magic: auto-wiring can make it hard to see what's injected.
- Indirection: jumping to the implementation takes an extra step.
| If… | Use instead |
|---|---|
| A value never changes and has no side effects | Just create it, or pass the value |
| Only one place uses a small helper | Construct it there |
| A large app with hundreds of services | A DI container, with explicit configuration |
| You're tempted to look dependencies up globally | Not a service locator or 05 Singleton: inject |
8. How it goes wrong
container and calling container.get(...) inside hides dependencies again. Inject the things themselves.LumenGateway(), switching providers is a hunt. Keep construction in the composition root.9. Remember it and say it
How this hook works: what “The chef doesn't grow the vegetables” means
What the slogan means, phrase by phrase
- “The chef”
RentalService.- “doesn't grow”
- Creates none of its dependencies.
- “handed”
- Constructor parameters.
Why this picture fits: Kitchens work with whatever suppliers deliver, and practice ingredients work just as well for an exam.
The story → the concept
| The kitchen | The code |
|---|---|
| The chef | RentalService |
| Suppliers | The composition root |
| Real produce | LumenGateway, WallClock, PushNotifier |
| Practice ingredients | FakeGateway, FixedClock, RecordingNotifier |
Watch it on an example
- A test builds the service with a fake gateway, a clock fixed at 23:00 and a recording notifier.
start_rental("leo")sees 23:00 and charges 0p.- The test checks the fake's charges and the recorded message. No real money, no real phone.
The trap it saves you from
Classes that build their own dependencies, so tests use real systems and real time.
It still works when…
Yes: web frameworks inject request handlers' services, test frameworks provide fixtures, and Angular, Spring and .NET are built around it.
In an interview or code review
- Say what's injected and why: "Payments, clock and notifier are injected, because they're costly, time-dependent or external."
- Name the composition root: production wiring lives in one function; tests build their own.
- Link it to SOLID: it's dependency inversion in practice.
- Container or not: by hand for small apps; a container for large ones, configured explicitly.
10. Check yourself
Where does RentalService get its payment gateway?
What is the composition root?
How do you test the late-night rule?
Passing a container and calling container.get() inside a service is…
A constructor needs ten dependencies. What does it suggest?
11. Go deeper
| Resource | Why |
|---|---|
| Low-level design 03e: Dependency inversion | The principle behind the pattern. |
| 05 Singleton | What injection usually replaces. |
| Martin Fowler: Inversion of Control Containers and the Dependency Injection pattern | The article that named the pattern. |
| Wikipedia: Dependency injection | Forms of injection and containers. |