Skip to content
FollowtheDamnCode

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

  1. The high-level module declares the interface it needs (OrderRepository, Notifier), in its own terms.
  2. Low-level modules implement it (SqlOrderRepository, EmailNotifier).
  3. Something at the edge — main, a factory, a DI container — creates the concrete objects and passes them in (dependency injection).
The policy owns the interfaces; details point up to them

In code

Business rules tested with in-memory parts
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) sent

Run 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 main are 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

ProsCons
Business rules testable without infrastructureMore interfaces and wiring code
Swap infrastructure without touching policyIndirection makes "what actually runs?" less obvious
Policy stays stable while details churnOverused, every class gets a pointless interface

How it relates

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.