Course contents

Dependency Inversion Principle

Sign in to run this lab and save your progress

Learn

Before you start

DIP is the last SOLID letter, and the one that most changes how you WRITE code once you get it. Every class you build after this will take its dependencies as constructor arguments instead of creating them inside.

The one-line version

Depend on interfaces, not on specific classes. Pass dependencies in instead of building them inside.

Why this is the highest-leverage principle

Every earlier principle improved the shape of a single class. DIP improves how classes CONNECT.

Watch what changes when you follow DIP consistently:

  • Testing. Every class becomes testable with fakes / mocks / stubs because you pass the dependency in from outside.
  • Reuse. The same OrderService works with email, SMS, push, or a queue — just pass a different notifier.
  • Swappable. The day you switch from Twilio to MessageBird, you replace ONE thing (the notifier). Every caller stays the same.
  • Fast tests. No real HTTP calls in unit tests. Test suite runs in seconds instead of minutes.

For any codebase of real size, this is the difference between "tests we trust" and "tests we skip".

The flip, in a picture

Before DIP:

OrderService  --calls-->  EmailNotifier  --calls-->  SMTP
(high-level)              (low-level)

OrderService knows about EmailNotifier. If you swap SMTP for an API, both classes change.

After DIP:

OrderService  --depends on-->  Notifier <--implements-- EmailNotifier
(high-level)                    (interface)             (low-level)

Both high-level (OrderService) and low-level (EmailNotifier) point at the interface. Neither knows about the other. That's the flip.

In interviews

Interviewers use DIP as a probe by asking "how would you test this?"

  • Weak answer: "I'd mock the email library with unittest.mock at the module level."
  • Strong answer: "The notifier is passed in. In the test I pass a fake notifier that captures the calls."

The second answer shows design maturity. It also works everywhere: every dependency (database, HTTP client, clock, random) should be passed in the same way.

About the strict test

The last test in this lesson reads your constructor's source code and fails if it names EmailNotifier or SmsNotifier. This is the strictest possible check — your constructor should only mention the abstract Notifier parameter and store it. Any specific type leaking in is DIP breaking down.

The end of SOLID

After this lesson you have every SOLID principle. The next lesson (the refactor drill) makes you use all five on a broken codebase — the hardest lesson in the module by design, because that's the interview shape.

Your task

D in SOLID, the last letter. Uncle Bob's version has two parts:

  1. High-level code should not depend on low-level code. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

In everyday words: pass your dependencies IN through the constructor. Don't create them inside the class.

The smell: hardwired dependency

class EmailNotifier:
    def send(self, to, message):
        # Imagine this really sends an email
        return f"emailed {to}: {message}"


class OrderService:
    def __init__(self):
        self._notifier = EmailNotifier()   # <-- hardwired

    def place_order(self, user_email, item):
        # (order-placing logic left out)
        return self._notifier.send(user_email, f"ordered {item}")

Problems:

  • Hard to test. Every unit test of OrderService.place_order tries to send a real email. You end up doing tricky patches at the module level, which is fragile.
  • Can't swap. Want to send via SMS for one campaign? Every user of OrderService has to change.
  • High-level knows about low-level. OrderService (business logic) knows about EmailNotifier (delivery mechanism). The dependency arrow points the wrong way.

The fix: pass it in through the constructor

class Notifier(ABC):
    @abstractmethod
    def send(self, to, message): ...


class EmailNotifier(Notifier):
    def send(self, to, message):
        return f"emailed {to}: {message}"


class OrderService:
    def __init__(self, notifier: Notifier):
        self._notifier = notifier    # <-- passed in

    def place_order(self, user_email, item):
        return self._notifier.send(user_email, f"ordered {item}")

Now:

  • Testable. Unit test passes a fake notifier that records the calls. No real emails.
  • Swappable. Want SMS? Pass an SmsNotifier — no change to OrderService.
  • Arrows point the right way. OrderService depends on the Notifier interface. So does EmailNotifier. Both high-level and low-level depend on the shared interface.

The "inversion" in the name

The old dependency direction: high-level code calls low-level utility code. OrderService calls EmailNotifier, which calls SMTP.

DIP flips this: OrderService calls a Notifier interface, and EmailNotifier implements that interface. The low-level class now depends on the interface, not the other way around.

That flip is what makes the code testable, swappable, and reusable.

Your task

Build the DIP-clean version in main.py:

  1. Abstract Notifier(ABC) with abstract send(self, to, message) returning a string.
  2. Concrete EmailNotifier(Notifier) whose send returns "emailed <to>: <message>".
  3. Concrete SmsNotifier(Notifier) whose send returns "texted <to>: <message>".
  4. OrderService:
    • Constructor takes a notifier: Notifier and stores it as self._notifier
    • Constructor does NOT construct any notifier internally
    • Has place_order(self, user_email, item) that calls self._notifier.send(user_email, f"ordered {item}") and returns the result
  5. WelcomeService:
    • Same pattern — takes a notifier in the constructor
    • Has welcome(self, user_email) that returns self._notifier.send(user_email, "welcome!")

The tests use a FakeNotifier that records every call, and swap it into both services. That's the DIP payoff — tests drive the design.

Click Submit to run the tests.

Starter files

Preview only — open the lab to edit and run.

files_dir
05-dependency-inversion-starter