Dependency Inversion Principle
High-level policy shouldn't depend on low-level details — both depend on an abstraction the policy owns, and the details get plugged in.
Big idea
The Dependency Inversion Principle (the D in SOLID): high-level modules shouldn't depend on low-level modules; both should depend on abstractions. The business rule defines the interface it needs ("I need somewhere to save orders"); the database code implements it. The arrow of source-code dependency points up, toward the policy, even though calls flow down.
The problem it solves
OrderService creates a MySqlDatabase and an SmtpClient inside its constructor. Consequences:
- You can't test order rules without a database and a mail server.
- Moving to Postgres means editing
OrderService, the most important class in the system. - The business rules — the part that should be most stable — change whenever infrastructure does.
Think of it like
A building's electrical sockets follow a national standard. Lamp makers build to the standard; the electrician wires to the standard; neither knows about the other. The building (high-level) defines the socket; appliances (low-level) conform. Swap your lamp and nobody rewires a wall.
How it works
- The high-level module declares the interface it needs (
OrderRepository,Notifier), in its own terms. - Low-level modules implement it (
SqlOrderRepository,EmailNotifier). - Something at the edge —
main, a factory, a DI container — creates the concrete objects and passes them in (dependency injection).
In code
class InMemoryOrders: # a detail
def __init__(self):
self.rows = {}
def save(self, order):
self.rows[order["id"]] = order
class ListNotifier: # a detail
def __init__(self):
self.sent = []
def notify(self, message):
self.sent.append(message)
class OrderService: # the policy: knows roles, not products
def __init__(self, orders, notifier): # injected from outside
self.orders, self.notifier = orders, notifier
def place(self, order):
if order["total"] <= 0:
raise ValueError("empty order")
self.orders.save(order)
self.notifier.notify(f"order {order['id']} confirmed")
orders, notifier = InMemoryOrders(), ListNotifier()
service = OrderService(orders, notifier) # wiring happens at the edge
service.place({"id": 7, "total": 42.0})
print(sorted(orders.rows))
print(notifier.sent)
try:
service.place({"id": 8, "total": 0})
except ValueError as err:
print("rejected:", err)
print(len(notifier.sent), "message(s) sent")output
[7]
['order 7 confirmed']
rejected: empty order
1 message(s) sentRun and edit it in the app.
Swap InMemoryOrders for a real database class in production — OrderService doesn't change, and its tests never need a database.
Under Dependency Inversion, who should define the OrderRepository interface?
Watch it
Inversion vs injection vs containers
- Dependency inversion — the principle: depend on abstractions owned by the policy.
- Dependency injection — the technique: pass collaborators in (constructor, parameter) instead of creating them inside.
- DI container — a tool that builds the object graph for you. Optional; plain constructors in
mainare often enough.
When to use it — and when not to
Invert at boundaries with volatile or slow things: databases, network services, file systems, clocks, randomness, third-party SDKs. Don't wrap stable, pure things — you don't need an interface in front of math.sqrt or the standard list type.
Pros and cons
| Pros | Cons |
|---|---|
| Business rules testable without infrastructure | More interfaces and wiring code |
| Swap infrastructure without touching policy | Indirection makes "what actually runs?" less obvious |
| Policy stays stable while details churn | Overused, every class gets a pointless interface |
How it relates
- Builds on Interfaces and Abstract Classes and enables Open/Closed.
- Factory Method and Abstract Factory are common ways to supply the concrete parts.
- It's the backbone of the LLD case studies: each one keeps rules separate from storage and I/O.
class Report: def __init__(self): self.db = PostgresClient('prod') — what's the main problem?
Key terms
- Dependency Inversion Principle — High-level and low-level code both depend on abstractions, and the abstraction belongs to the high-level side.
- Dependency injection — Supplying an object's collaborators from outside rather than creating them inside.
- Composition root — The one place (often
main) where concrete objects are created and wired together.