InterviewKata
Low-level design · Module 03e of 11 · Level 1: Foundations

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

A cinema's box office works the same whichever bank processes its cards, whichever clock is on the wall and whichever company prints its tickets. The box office writes down what it needs ("take a payment", "tell me the time", "send a confirmation"), and the suppliers fit that description. Nobody rebuilds the box office to change bank.

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".

TermWhat it isRelation to the principle
Dependency inversionA design principle: who owns the interface, which way dependencies pointThe goal
Dependency injectionA technique: give an object its collaborators (constructor, method or setter) instead of letting it create themThe usual way to achieve it
Composition rootThe one place (often main) where concrete classes are chosen and wiredWhere all the "new"s live
IoC containerA library that builds the object graph from registrations (Spring, .NET's built-in container, Guice, NestJS)Automates the composition root in large apps
Service locatorObjects ask a global registry for what they need at run timeInverts too, but hides dependencies inside methods; most teams prefer injection
Plugin architectureThe core defines interfaces; plugins implementing them are found and loaded at start-upThe 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.

Payment
Clock
Notifier

      

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

6. Remember it

Memory hook: "Policy owns the plug"High-level code defines the interfaces it needs; details implement them and are plugged in from outside.
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 cinemaThe code
Box officeBookingService
"Take a payment"PaymentGateway, owned by booking
The bank's terminalA card provider adapter
Changing banksOne line in the composition root

Watch it on an example

  1. Production plugs in the card provider, system clock and email.
  2. Tests plug in a declining gateway, a fixed clock and a recording notifier.
  3. 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

new inside business logicConcrete details hard-wired into policy can't be swapped or tested.
Interfaces shaped like the vendor's SDKThen the "abstraction" changes whenever the vendor's does; shape it by what your code needs.
A service locator everywhereDependencies hide inside methods; you only find them when a test fails.
Interfaces for everythingInverting pure helpers and value objects adds files without adding flexibility.

8. Practice exercise (25 minutes)

  1. Find the news (5 min). In your last LLD solution, list every place business logic creates a concrete dependency.
  2. Own the interface (10 min). Define the interface in the business code's words, and move the creation to main.
  3. Inject time (5 min). Replace any direct clock call with an injected Clock and write a test at a fixed time.
  4. 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?

You can inject without inverting, and invert with plain constructor arguments.

Where should the PaymentGateway interface live?

The high-level side owns the abstraction; details depend on it.

Why do many teams avoid the service locator pattern?

With constructor injection, the dependencies are visible in the signature.

What's the composition root?

Usually main or the app's start-up code; a container can generate it.

10. Go deeper

ResourceTimeWhy
Module 03: SOLID in the interviewNextAll five on one class, and the questions interviewers ask.
Inside a service: hexagonal and clean architecture (Software architecture)30 minDependency inversion as the rule behind ports and adapters.
P12: Movie ticket booking45 minThe full problem this module's example comes from.
Inversion of Control Containers and the Dependency Injection pattern (Martin Fowler)20 minWhere "dependency injection" and "service locator" were named and compared.