Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Low-Level Design (Object-Oriented Design)  Module 4 – Design principles: SOLID and beyond

Dependency Inversion Principle (DIP)

The Dependency Inversion Principle in practice: make business rules own their abstractions, inject the details, and keep DIP, DI and containers apart.

  • Intermediate
  • 25 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Design high-level policy that depends only on abstractions it owns and names in its own terms
  • Build the object graph in one composition root, passing collaborators through constructors
  • Distinguish dependency inversion, dependency injection and injection containers
  • Test time-based rules exactly by passing in a clock and an id source

Before you start

What you’ll learn · Before you start

What you will learn

  • Design high-level policy that depends only on abstractions it owns and names in its own terms
  • Build the object graph in one composition root, passing collaborators through constructors
  • Distinguish dependency inversion, dependency injection and injection containers
  • Test time-based rules exactly by passing in a clock and an id source

Before you start

On this page

The Dependency Inversion Principle (DIP) says that the code holding your business rules should not depend on the code that talks to the outside world. Both should depend on an abstraction, and that abstraction belongs to the rules: it is written in their terms and lives next to them. The details, such as an SMS gateway, a database or the system clock, implement it. DIP is the last of the five SOLID principles. This lesson applies it to a service that no test can build, and then separates it from dependency injection and injection containers, which are often mistaken for it.

What the principle says

Robert C. Martin set out the principle in 1996, in a column for The C++ Report, as two rules. The first:

High level modules should not depend upon low level modules. Both should depend upon abstractions.

The second:

Abstractions should not depend upon details. Details should depend upon abstractions.

He chose the word “inversion” because the design methods of the time produced the opposite. A program was planned as a tree of calls, and each module depended on the modules it called: the rules at the top depended on the helpers at the bottom. Dependency is transitive, so a policy that uses a mechanism that uses a utility depends on that utility too, and a change at the bottom can force changes all the way up. Martin’s point was that the rules are what we want to keep and reuse, so they are the part that should depend on nothing volatile.

Inverting does not change which way the calls go: the rules still call the SMS code at run time. It changes which way the source code points. The rules define an interface, the SMS code imports that interface and implements it, and so the arrow in the source runs from the detail to the rules. Martin’s later one-line version is “Depend in the direction of abstraction.”

A service that builds its own details

A gym sends each member one SMS when their membership is about to end. The first version of the service does everything itself: it creates its own SmsGateway, which needs the key of a live account, reads the members from a module-level list and asks date.today() what day it is. Here is what happens when a test tries to build it:

A test that cannot even build the service

before/try_to_test.py

"""A first attempt at testing the reminders: the test cannot even build the object."""

from reminders import RenewalReminders

try:
    reminders = RenewalReminders()
except RuntimeError as error:
    print("cannot build RenewalReminders in a test:")
    print(" ", error)

Output

cannot build RenewalReminders in a test:
  SMS_API_KEY is not set, and the gateway cannot start without a live account

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 try_to_test.py

before/reminders.py

"""Membership renewal reminders for a gym, first version: the service builds and finds everything it needs itself."""

from datetime import date

from sms_gateway import SmsGateway

# In the real program these rows come from the gym's database.
MEMBERS = [
    ("Asha", "+91 00000 11111", date(2025, 3, 15)),
    ("Ravi", "+91 00000 22222", date(2025, 3, 18)),
]


class RenewalReminders:
    def __init__(self):
        self.gateway = SmsGateway()
        self.reminded = set()

    def run(self) -> int:
        today = date.today()
        sent = 0
        for name, phone, ends_on in MEMBERS:
            days_left = (ends_on - today).days
            if 0 <= days_left <= 3 and name not in self.reminded:
                self.gateway.send_sms(phone, f"Hi {name}, your membership ends in {days_left} days.")
                self.reminded.add(name)
                sent += 1
        return sent

before/sms_gateway.py

"""A client for an SMS provider's web API. Building one needs a live account's key."""

import json
import os


class SmsGateway:
    URL = "https://sms.example.com/v1/messages"

    def __init__(self):
        key = os.environ.get("SMS_API_KEY")
        if not key:
            raise RuntimeError("SMS_API_KEY is not set, and the gateway cannot start without a live account")
        self._key = key

    def send_sms(self, phone: str, text: str) -> None:
        from urllib.request import Request, urlopen

        body = json.dumps({"to": phone, "text": text}).encode()
        request = Request(self.URL, data=body, headers={"Authorization": f"Bearer {self._key}"})
        with urlopen(request, timeout=10):
            pass

The constructor fails because the gateway wants a live key. Giving the test a real key would be worse: the test would send real messages to real members, and its result would depend on the day it runs, because the service reads the system clock. Three things the rules need are hidden inside the class, a gateway, a clock and the member data, and every one of them is a detail.

Inverting the dependency

In the second version, reminders.py holds the rules and the two abstractions they need. Notifier has one method, remind(member, days_left), and Clock has today(). RenewalReminders receives a notifier, a clock and the members in its constructor and knows nothing about SMS. The SMS code moves to sms_notifier.py, which imports Member from reminders.py and implements Notifier with the gateway. A test can now pass a notifier that only records what it was asked to send, and a clock fixed on one day:

The same rules, tested with a fixed clock and a recording notifier

after/test_reminders.py

"""Tests of the reminder policy with a fixed clock and a notifier that only records: no network, no key, no date."""

from datetime import date

from reminders import Member, RenewalReminders


class FixedClock:
    def __init__(self, day):
        self.day = day

    def today(self):
        return self.day


class RecordingNotifier:
    def __init__(self):
        self.sent = []

    def remind(self, member, days_left):
        self.sent.append((member.name, days_left))


members = [
    Member("Asha", "+91 00000 11111", date(2025, 3, 15)),  # ends in 3 days on the 12th
    Member("Kabir", "+91 00000 33333", date(2025, 3, 12)),  # ends on the 12th itself
    Member("Meera", "+91 00000 44444", date(2025, 3, 11)),  # ended the day before
    Member("Ravi", "+91 00000 22222", date(2025, 3, 18)),  # 6 days away on the 12th
]
clock = FixedClock(date(2025, 3, 12))
notifier = RecordingNotifier()
reminders = RenewalReminders(members, notifier, clock)

reminders.run()
print("on 2025-03-12:", notifier.sent)
assert notifier.sent == [("Asha", 3), ("Kabir", 0)]

notifier.sent.clear()
reminders.run()
print("the same day again:", notifier.sent)
assert notifier.sent == []

clock.day = date(2025, 3, 15)
reminders.run()
print("on 2025-03-15:", notifier.sent)
assert notifier.sent == [("Ravi", 3)]
print("all three checks passed")

Output

on 2025-03-12: [('Asha', 3), ('Kabir', 0)]
the same day again: []
on 2025-03-15: [('Ravi', 3)]
all three checks passed

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 test_reminders.py

after/reminders.py

"""Membership renewal reminders: the policy, and the two abstractions it needs, which it owns."""

from collections.abc import Iterable
from dataclasses import dataclass
from datetime import date
from typing import Protocol


@dataclass(frozen=True)
class Member:
    name: str
    phone: str
    ends_on: date


class Notifier(Protocol):
    """Tells a member that their membership is about to end. Named in the policy's words, not a channel's."""

    def remind(self, member: Member, days_left: int) -> None: ...


class Clock(Protocol):
    def today(self) -> date: ...


class RenewalReminders:
    """Reminds each member once, when their membership ends within `days_ahead` days (today included)."""

    def __init__(self, members: Iterable[Member], notifier: Notifier, clock: Clock, days_ahead: int = 3):
        self._members = list(members)
        self._notifier = notifier
        self._clock = clock
        self._days_ahead = days_ahead
        self._reminded = set()

    def run(self) -> int:
        today = self._clock.today()
        sent = 0
        for member in self._members:
            days_left = (member.ends_on - today).days
            if 0 <= days_left <= self._days_ahead and member.name not in self._reminded:
                self._notifier.remind(member, days_left)
                self._reminded.add(member.name)
                sent += 1
        return sent

after/sms_notifier.py

"""The SMS detail. It implements the Notifier that reminders.py defines, so it depends on the policy."""

from reminders import Member
from sms_gateway import SmsGateway


class SmsNotifier:
    def __init__(self, gateway: SmsGateway):
        self._gateway = gateway

    def remind(self, member: Member, days_left: int) -> None:
        when = "today" if days_left == 0 else f"in {days_left} days"
        self._gateway.send_sms(member.phone, f"Hi {member.name}, your gym membership ends {when}.")

after/system_clock.py

"""The real clock, a detail like the SMS gateway."""

from datetime import date


class SystemClock:
    def today(self) -> date:
        return date.today()

after/sms_gateway.py

"""A client for an SMS provider's web API. Building one needs a live account's key."""

import json
import os


class SmsGateway:
    URL = "https://sms.example.com/v1/messages"

    def __init__(self):
        key = os.environ.get("SMS_API_KEY")
        if not key:
            raise RuntimeError("SMS_API_KEY is not set, and the gateway cannot start without a live account")
        self._key = key

    def send_sms(self, phone: str, text: str) -> None:
        from urllib.request import Request, urlopen

        body = json.dumps({"to": phone, "text": text}).encode()
        request = Request(self.URL, data=body, headers={"Authorization": f"Bearer {self._key}"})
        with urlopen(request, timeout=10):
            pass

after/main.py

"""The composition root: the one place that builds the real objects and wires them together."""

from datetime import date

from reminders import Member, RenewalReminders
from sms_gateway import SmsGateway
from sms_notifier import SmsNotifier
from system_clock import SystemClock


def load_members() -> list[Member]:
    # In the real program, a query against the gym's database.
    return [Member("Asha", "+91 00000 11111", date(2025, 3, 15)), Member("Ravi", "+91 00000 22222", date(2025, 3, 18))]


def build_reminders() -> RenewalReminders:
    return RenewalReminders(load_members(), notifier=SmsNotifier(SmsGateway()), clock=SystemClock())


if __name__ == "__main__":
    print("reminders sent:", build_reminders().run())

The three checks pin down the rules exactly: a membership that ends in three days or today gets a reminder, one that ended yesterday or is six days away does not, and nobody is reminded twice. None of this needs a network, a key or a particular date. This program reads the imports of both versions and prints which of the program’s own modules each one imports:

Which module imports which, before and after Python · imports.py
"""Prints which of the program's own modules import which, before and after the inversion (standard library left out)."""

import ast
from pathlib import Path


def own_imports(folder):
    modules = {path.stem: path for path in sorted(Path(folder).glob("*.py")) if "test" not in path.stem}
    graph = {}
    for name, path in modules.items():
        tree = ast.parse(path.read_text(encoding="utf-8"))
        found = set()
        for node in ast.walk(tree):
            if isinstance(node, ast.ImportFrom) and node.module in modules:
                found.add(node.module)
            elif isinstance(node, ast.Import):
                found.update(alias.name for alias in node.names if alias.name in modules)
        graph[name] = sorted(found)
    return graph


for folder in ("before", "after"):
    print(f"{folder}:")
    for module, imported in own_imports(folder).items():
        print(f"  {module} -> {', '.join(imported) or '(none of ours)'}")

Output

before:
  reminders -> sms_gateway
  sms_gateway -> (none of ours)
after:
  main -> reminders, sms_gateway, sms_notifier, system_clock
  reminders -> (none of ours)
  sms_gateway -> (none of ours)
  sms_notifier -> reminders, sms_gateway
  system_clock -> (none of ours)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 imports.py

Before, reminders imports sms_gateway: the rules depend on the detail. After, reminders imports none of the program’s other modules, and sms_notifier imports reminders. That reversed arrow is the inversion the principle is named after.

Before, reminders.py imports sms_gateway.py. After, sms_notifier.py imports reminders.py, and main.py imports and wires all four.1. Before: the policy imports the detail2. After: the detail imports the policyreminders.pyRenewalReminderssms_gateway.pySmsGatewayreminders.pyRenewalReminders,Notifier, Clocksms_notifier.pySmsNotifiersms_gateway.pySmsGatewaysystem_clock.pySystemClockmain.pycomposition rootAn arrow pointsfrom a module toone that it importsimports and buildsimplements Notifier

Which module imports which, before and after the inversion

Text description of the diagram

The diagram shows the imports of the reminder program in two versions, one above the other. Each arrow points from a module to a module it imports.

  1. Before: reminders.py, which holds the RenewalReminders policy, imports sms_gateway.py and builds an SmsGateway itself. The policy depends on the detail.
  2. After: reminders.py holds RenewalReminders and the two abstractions it needs, Notifier and Clock, and imports none of the program's other modules. sms_notifier.py imports reminders.py, because SmsNotifier implements Notifier, and it imports sms_gateway.py to send the messages. system_clock.py holds the real clock. main.py, the composition root, imports all four modules and wires the real objects together. The arrow between the policy and the SMS detail now points from the detail to the policy.

Who owns the abstraction

The second rule is the one most often missed. An interface that copies the SMS provider’s API, with a phone number, a sender id and a template id, is an abstraction that depends on a detail. The rules would still change when the provider changes, and a fake or an email channel would have to pretend to send SMS messages. Notifier.remind(member, days_left) is named in the rules’ own words, so the SMS channel, an email channel and a test fake all fit it, and each one decides for itself how to word its message.

That is also why the interface sits in reminders.py. The Go project gives the same advice for Go code: declare an interface in the package that consumes it, and let the package that provides the implementation return a concrete type. Put the interface with the code that needs it, and shape it by that code’s needs, as the previous lesson did for the gate devices.

Dependency injection and the composition root

Passing a collaborator in, instead of letting a class create it, is called dependency injection. Martin Fowler named it in 2004, after he and others found “inversion of control” too generic a term for what the new frameworks did. He described three forms: constructor injection, setter injection and interface injection. This lesson uses constructor injection throughout, because an object that receives everything in its constructor is complete as soon as it exists, and its constructor lists everything it depends on.

Something still has to create the real gateway, notifier and clock. Mark Seemann calls that place the composition root: one place, as close as possible to the program’s entry point, where the objects are composed. Here it is build_reminders() in main.py, the only code that names SmsGateway and SystemClock. Seemann adds that only applications have a composition root. A library receives its dependencies and leaves the wiring to the program that uses it.

Inversion, injection and containers are different things

The three ideas are often blurred together, so it helps to set them side by side:

Dependency inversion, dependency injection and injection containers
Criterion Dependency inversionDependency injectionInjection container
What it is A design principle about which way source dependencies pointA technique: collaborators are passed inA library that builds objects from registrations
Can exist without the others Yes: an abstract class whose subclasses supply the detailsYes: passing a concrete SmsGateway in still inverts nothingRarely useful alone; it automates injection
Needed in a design interview Yes, at the boundaries that changeYes, through constructorsNo: wire the objects by hand in main
When to choose Rules that must not depend on volatile detailsEvery collaborator that a test may want to replaceLarge applications with many lifetimes and scopes

Injection without inversion is common: a constructor that takes an SmsGateway lets a test pass a subclass, but the rules still import the gateway’s module and use its methods. Inversion without injection also exists, and Martin gave it as an example of the principle: in the template method pattern, a base class holds the rules and calls abstract methods that its subclasses fill in. Most designs combine the two, and none of them needs a container.

Inject the clock and the id source too

The clock deserves its own section, because tests often try to fake it by patching instead. In Python that does not even work for the standard clock:

The standard library's clock cannot be swapped by assignment Python · clock/patch_the_clock.py
"""Why tests cannot simply swap the standard library's clock for a fixed one."""

import datetime


def fixed_day():
    return datetime.date(2025, 3, 12)


try:
    datetime.date.today = fixed_day
except TypeError as error:
    print("patching date.today():", error)
print("date.today() is still the real clock:", datetime.date.today is not fixed_day)

Output

patching date.today(): cannot set 'today' attribute of immutable type 'datetime.date'
date.today() is still the real clock: True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 patch_the_clock.py

datetime.date is a built-in type, and Python refuses to set its attributes. Libraries exist that patch time in other ways, but each test then depends on how the code under test happens to read the clock. A Clock passed in, with today() or now(), is plain code that any test controls. The same goes for anything else that differs between runs. Pass an id generator instead of calling uuid4() inside the rules, and pass a random.Random instance, which the Python documentation describes as a generator that does not share state with the module’s functions, instead of calling random.random(). A test can then seed it.

The principle is not tied to Python. The same rules in JavaScript receive any object with remind() and today(), and the same three checks pass:

The reminder rules in JavaScript, with the same fakes

js/check.mjs

// The same three checks as test_reminders.py, with a fixed clock and a notifier that only records.
import { RenewalReminders } from './reminders.mjs';

const march = (day) => Date.UTC(2025, 2, day); // months count from 0 in JavaScript
const clock = { day: march(12), today() { return this.day; } };
const sent = [];
const notifier = { remind(member, daysLeft) { sent.push(`${member.name} (${daysLeft})`); } };

const reminders = new RenewalReminders({
  members: [
    { name: 'Asha', endsOn: march(15) },
    { name: 'Kabir', endsOn: march(12) },
    { name: 'Meera', endsOn: march(11) },
    { name: 'Ravi', endsOn: march(18) },
  ],
  notifier,
  clock,
});

const runOn = (label) => {
  sent.length = 0;
  reminders.run();
  console.log(`${label}: ${sent.join(', ') || 'nobody'}`);
  return sent.join(', ');
};

const results = [runOn('on 2025-03-12'), runOn('the same day again')];
clock.day = march(15);
results.push(runOn('on 2025-03-15'));
const expected = ['Asha (3), Kabir (0)', '', 'Ravi (3)'];
console.log(results.every((r, i) => r === expected[i]) ? 'all three checks passed' : 'a check failed');

Output

on 2025-03-12: Asha (3), Kabir (0)
the same day again: nobody
on 2025-03-15: Ravi (3)
all three checks passed

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node check.mjs

js/reminders.mjs

// The reminder policy in JavaScript. It receives its notifier and its clock; any object with remind() and today()
// will do, which is how JavaScript expresses the two abstractions.
const DAY_MS = 24 * 60 * 60 * 1000;

export class RenewalReminders {
  #members;
  #notifier;
  #clock;
  #daysAhead;
  #reminded = new Set();

  constructor({ members, notifier, clock, daysAhead = 3 }) {
    this.#members = [...members];
    this.#notifier = notifier;
    this.#clock = clock;
    this.#daysAhead = daysAhead;
  }

  run() {
    const today = this.#clock.today();
    let sent = 0;
    for (const member of this.#members) {
      const daysLeft = Math.round((member.endsOn - today) / DAY_MS);
      if (daysLeft >= 0 && daysLeft <= this.#daysAhead && !this.#reminded.has(member.name)) {
        this.#notifier.remind(member, daysLeft);
        this.#reminded.add(member.name);
        sent += 1;
      }
    }
    return sent;
  }
}

When not to invert

Not every dependency needs an abstraction. Code that is stable and has no side effects, such as the standard library’s data structures, a Money value object or a pure pricing function, can be used directly: an interface in front of it adds a name and a file and protects against nothing. Invert where the detail is likely to change or is awkward in a test: the network, files, databases, the clock, randomness and third-party software. The Go guidance quoted above adds a useful brake: do not define an interface before there is a real use for it.

In a design interview

Draw the rules in the middle and the details around them, with the arrows pointing inwards, and say which interface each detail implements: “the booking service owns a PaymentGateway interface; the card adapter implements it; main wires them”. Then write the tests against fakes. When the interviewer asks for a second payment provider, the answer is one new adapter and one line in main.

Python Online Compiler Add an email notifier to the inverted version and wire it in main.py without touching reminders.py. JavaScript & TypeScript Online Compiler Change the fake clock in check.mjs and watch which members get a reminder.

Interview questions

Warm-up (fresher to mid level): what is the difference between dependency inversion and dependency injection? Dependency inversion is a design principle about the direction of source-code dependencies: high-level rules and low-level details both depend on an abstraction, and the abstraction is owned by the rules, so the details import the rules’ interface rather than the other way round. Dependency injection is a technique: an object receives its collaborators, usually through its constructor, instead of creating them. Injection is the usual way to apply inversion, because the rules receive an implementation of their own interface, but the two are independent. Passing a concrete gateway class into a constructor is injection that inverts nothing, and a base class whose subclasses supply the details inverts without injecting anything. An injection container is a third thing, a library that automates the wiring; neither idea needs one.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    Your InvoiceService calls datetime.now(), uuid4() and a tax-rate web API directly. Its tests fail around midnight and on the last day of each month, and they need the network. How do you make it testable without a dependency injection framework?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A teammate puts an interface in front of every class "for DIP": IUserService, IUserRepository, IDateFormatter and IMoney, each with exactly one implementation. Where is the inversion worth its cost, and where is it noise? Give a rule you would defend in a design discussion.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • DIP: rules and details both depend on an abstraction, and the abstraction belongs to the rules, named in their terms and kept next to them.
  • The calls still go from the rules to the details; inversion reverses the source dependency, so the details import the rules’ interface.
  • Inject collaborators through constructors, and build the real objects in one composition root near the entry point.
  • Inversion, injection and containers are different: you can have either of the first two without the other, and neither needs a container.
  • Inject clocks, id sources and random generators; invert at volatile boundaries, not in front of stable code.

Exercise

Exercise · Medium · Python

Inject the gateway, clock and ids of an order service

A canteen's OrderService builds its own LiveCardGateway, reads the real clock with datetime.now() and makes a random order id with uuid4(). No test can check one of its receipts: the gateway cannot be reached from a test, and the time and the id change on every run.

Change it so that all three are passed in:

  • OrderService(gateway, clock, new_id): gateway has charge(amount, card_token), which returns a payment id; clock has now(), which returns a datetime; new_id is a function with no arguments that returns the next order id. Keep the attribute names gateway, clock and new_id.
  • place_order() keeps its receipt format, order <id> placed at <YYYY-MM-DD HH:MM> UTC: <total> charged, payment <payment id>, and still refuses an empty order with ValueError before anything is charged.
  • Add build_live_service(), the composition root: it returns an OrderService wired with LiveCardGateway, a clock that returns datetime.now(timezone.utc) and the random ORD- ids. Building it must not charge anything.

The sample tests pass a fake gateway that records its charges, a clock that always returns the same moment and a fixed list of ids, and compare the receipts exactly.

Starter code · order_service.py

"""A canteen's order service. It builds its own payment gateway, reads the real clock and makes random ids, so no
test can check a receipt. Inject all three (see the prompt)."""

import uuid
from datetime import datetime, timezone


class LiveCardGateway:
    """Charges a card through the payment provider (not reachable from a test or from this page)."""

    def charge(self, amount: int, card_token: str) -> str:
        raise ConnectionError("the payment provider cannot be reached from here")


class OrderService:
    def __init__(self):
        self.gateway = LiveCardGateway()

    def place_order(self, items: list[tuple[str, int, int]], card_token: str) -> str:
        """items: (name, quantity, unit price) rows. Returns the receipt line."""
        if not items:
            raise ValueError("an order needs at least one item")
        total = sum(quantity * price for _, quantity, price in items)
        payment_id = self.gateway.charge(total, card_token)
        order_id = "ORD-" + uuid.uuid4().hex[:6].upper()
        placed_at = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
        return f"order {order_id} placed at {placed_at}: {total} charged, payment {payment_id}"
The sample tests · test_order_service.py
from datetime import datetime, timezone

import order_service
from order_service import LiveCardGateway, OrderService


class FakeGateway:
    def __init__(self):
        self.charges = []

    def charge(self, amount, card_token):
        self.charges.append((amount, card_token))
        return f"pay_{len(self.charges)}"


class FixedClock:
    def now(self):
        return datetime(2025, 3, 12, 9, 30, tzinfo=timezone.utc)


def make_service(gateway=None):
    ids = iter(["ORD-0001", "ORD-0002"])
    return OrderService(gateway=gateway or FakeGateway(), clock=FixedClock(), new_id=lambda: next(ids))


def test_receipt_is_exact():
    """with a fake gateway, a fixed clock and fixed ids, the receipt is known in advance"""
    service = make_service()
    receipt = service.place_order([("dosa", 2, 60), ("filter coffee", 1, 30)], "tok_asha")
    assert receipt == "order ORD-0001 placed at 2025-03-12 09:30 UTC: 150 charged, payment pay_1"


def test_second_order_uses_the_next_id():
    """each order takes the next id from the injected id source"""
    service = make_service()
    service.place_order([("idli", 3, 20)], "tok_asha")
    receipt = service.place_order([("vada", 1, 25)], "tok_ravi")
    assert receipt == "order ORD-0002 placed at 2025-03-12 09:30 UTC: 25 charged, payment pay_2"


def test_gateway_is_charged_the_total():
    """the gateway receives the order total and the card token"""
    gateway = FakeGateway()
    make_service(gateway).place_order([("dosa", 2, 60), ("filter coffee", 1, 30)], "tok_asha")
    assert gateway.charges == [(150, "tok_asha")]


def test_empty_order_charges_nothing():
    """an empty order is refused before anything is charged"""
    gateway = FakeGateway()
    try:
        make_service(gateway).place_order([], "tok_asha")
    except ValueError:
        pass
    else:
        raise AssertionError("an empty order must raise ValueError")
    assert gateway.charges == []


def test_composition_root_wires_the_live_parts():
    """build_live_service() wires the live gateway, a UTC clock and ORD- ids, without charging anything"""
    service = order_service.build_live_service()
    assert isinstance(service, OrderService)
    assert isinstance(service.gateway, LiveCardGateway)
    assert service.clock.now().tzinfo == timezone.utc
    order_id = service.new_id()
    assert order_id.startswith("ORD-") and len(order_id) == 10
A hint

Move each call that reaches outside the service into a constructor parameter: LiveCardGateway() becomes gateway, datetime.now(timezone.utc) becomes clock.now(), and the uuid expression becomes new_id(). The code you take out of place_order() does not disappear: the real clock and the random id move into a small class and a function that build_live_service() passes in.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 Which pair of rules is the Dependency Inversion Principle?

    Choose one answer.

    Show the answer to question 1

    Answer: High-level modules do not depend on low-level modules, both depend on abstractions; abstractions do not depend on details, details depend on abstractions

    These are the two rules of Robert C. Martin's 1996 column. The first says that the policy and the details meet at an abstraction; the second says that the abstraction is shaped by the policy, not by the details. Neither rule needs an interface for every class or a container.

  2. Question 2 of 6 In the inverted reminder program, why is Notifier defined in reminders.py and not in sms_notifier.py?

    Choose one answer.

    Show the answer to question 2

    Answer: The policy decides what it needs, so the SMS detail imports the policy's interface and implements it

    With the interface in the policy's module, the source dependency points from the detail to the policy, and the policy imports none of the program's details. If the interface lived next to the SMS code, the policy would still import the SMS module, and the dependency would not be inverted.

  3. Question 3 of 6 What does patch_the_clock.py print on CPython?

    What does this program print? Choose one answer.

    """Why tests cannot simply swap the standard library's clock for a fixed one."""
    
    import datetime
    
    
    def fixed_day():
        return datetime.date(2025, 3, 12)
    
    
    try:
        datetime.date.today = fixed_day
    except TypeError as error:
        print("patching date.today():", error)
    print("date.today() is still the real clock:", datetime.date.today is not fixed_day)
    Show the answer to question 3

    Answer: it prints

    patching date.today(): cannot set 'today' attribute of immutable type 'datetime.date'
    date.today() is still the real clock: True

    datetime.date is a built-in type, and Python refuses to set attributes on it, so the assignment raises TypeError and the real clock stays. A test that needs a fixed day has to receive a clock it can control, which is what injecting a Clock gives it.

  4. Question 4 of 6 Which of these statements are true?

    Choose every answer that is right.

    Show the answer to question 4

    Answer:

    • Dependency injection can be done by hand, by passing objects to a constructor
    • Passing a concrete SmsGateway into the constructor is injection, but the policy still depends on the detail's class
    • The Dependency Inversion Principle is about the direction of source-code dependencies

    Injection is a technique, inversion is a design rule, and a container is an optional library that automates the wiring. You can inject without inverting (pass a concrete class) and invert without a container (wire by hand in main). Stable classes such as data classes and the standard library need no abstraction at all.

  5. Question 5 of 6 Where should the program build the real SmsGateway?

    Choose one answer.

    Show the answer to question 5

    Answer: In the composition root, near the program's entry point, which wires the real objects together

    One composition root near the entry point is the only code that names the real details. The policy receives them, so it can be built in a test with fakes, and a new channel means a change in one place.

  6. Question 6 of 6 RenewalReminders depends on this abstraction. What is wrong with it?

    Read the code, then choose one answer.

    class Notifier(Protocol):
        def send_sms(self, phone: str, text: str, sender_id: str, template_id: str) -> dict: ...
    Show the answer to question 6

    Answer: It is written in the SMS provider's terms, so the abstraction depends on a detail and an email or a fake must pretend to be SMS

    The second rule says abstractions should not depend on details. An interface that copies one provider's API ties the policy to that provider: a new channel or a new provider changes the abstraction and the policy with it. Name the operation the way the policy thinks of it, such as remind(member, days_left).

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.