InterviewKata
Design patterns · 24 of 26 · Beyond the book

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

A restaurant kitchen doesn't grow its own vegetables or forge its own knives. Suppliers deliver them, and the chef cooks with whatever arrives. For a cookery-school exam, the same chef is handed practice ingredients, and nothing about how they cook changes.

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

RoleIts jobIn Pedal
Service (client)Declares what it needs, usually as constructor parameters; creates none of it.RentalService(payments, clock, notifier)
Dependency interfacesWhat the service relies on.PaymentGateway, Clock, Notifier
ImplementationsReal 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 changeWithout the patternWith it
Test the late-night ruleWait until 10 p.m., or patch the time modulePass FixedClock(23)
Switch payment providerEdit RentalServiceChange one line in the composition root
See what the service depends onRead its whole bodyRead 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.

Running as
payments
clock
notifier

      

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 effectsJust create it, or pass the value
Only one place uses a small helperConstruct it there
A large app with hundreds of servicesA DI container, with explicit configuration
You're tempted to look dependencies up globallyNot a service locator or 05 Singleton: inject

8. How it goes wrong

Injecting a service locatorPassing a container and calling container.get(...) inside hides dependencies again. Inject the things themselves.
Constructors with ten parametersThat's a class with too many jobs, not a DI problem. Split it.
Wiring scattered everywhereIf many places say LumenGateway(), switching providers is a hunt. Keep construction in the composition root.
Faking things that don't need itA pure pricing function needs no fake; test it directly. Inject what's slow, costly or unpredictable.

9. Remember it and say it

Memory hook: "The chef doesn't grow the vegetables"A class cooks with what it's handed; one place decides what gets delivered.
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 kitchenThe code
The chefRentalService
SuppliersThe composition root
Real produceLumenGateway, WallClock, PushNotifier
Practice ingredientsFakeGateway, FixedClock, RecordingNotifier

Watch it on an example

  1. A test builds the service with a fake gateway, a clock fixed at 23:00 and a recording notifier.
  2. start_rental("leo") sees 23:00 and charges 0p.
  3. 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

10. Check yourself

Where does RentalService get its payment gateway?

That's constructor injection.

What is the composition root?

Here, build_production().

How do you test the late-night rule?

Predictable time makes a predictable test.

Passing a container and calling container.get() inside a service is…

Inject the dependencies themselves.

A constructor needs ten dependencies. What does it suggest?

Split responsibilities.

11. Go deeper

ResourceWhy
Low-level design 03e: Dependency inversionThe principle behind the pattern.
05 SingletonWhat injection usually replaces.
Martin Fowler: Inversion of Control Containers and the Dependency Injection patternThe article that named the pattern.
Wikipedia: Dependency injectionForms of injection and containers.