Dependency Inversion Principle
The Dependency Inversion Principle (DIP), the D in SOLID, states that high-level modules should not depend on low-level modules — both should depend on abstractions. Its second part adds that abstractions should not depend on details; details (concrete implementations) should depend on abstractions.
In practice, the problem DIP addresses is a hardcoded dependency: a service class that implements a business rule directly instantiates the concrete storage class it needs. A schema change, a database migration, or a unit test all require modifying the service, even though the service's responsibility is to enforce business rules, not to manage connection strings.
The tightly coupled order service
A checkout system's OrderService must persist an order after validating it. The path of least
resistance is to instantiate a MySqlOrderRepository directly inside the service. The code works,
but the service is now welded to MySQL.
1# Bad: OrderService directly instantiates MySqlOrderRepository,
2# making the high-level policy depend on a low-level detail
3import mysql.connector
4
5class MySqlOrderRepository:
6 def __init__(self):
7 self._conn = mysql.connector.connect(
8 host="db.internal", user="app", password="s3cr3t", database="orders"
9 )
10
11 def save(self, order: "Order") -> None:
12 cursor = self._conn.cursor()
13 cursor.execute(
14 "INSERT INTO orders (id, total) VALUES (%s, %s)",
15 (order.id, order.total),
16 )
17 self._conn.commit()
18
19
20class OrderService:
21 def __init__(self):
22 # hardcoded: changing the DB requires editing the service
23 self._repo = MySqlOrderRepository()
24
25 def place_order(self, order: "Order") -> None:
26 if order.total <= 0:
27 raise ValueError("Order total must be positive")
28 self._repo.save(order)
The consequences are visible across the codebase. Running a unit test for the validation logic
requires a live MySQL instance. Switching to PostgreSQL means opening OrderService and editing
it. Adopting a different persistence strategy — say, an event-sourced store — requires rewriting
the service. The business rule and the storage decision are entangled despite being unrelated
concerns.
The fix is to introduce an abstraction — an OrderRepository interface — that the service
depends on, and to inject the concrete implementation from outside. The service accepts the
interface; the MySQL class implements it. Both classes point toward the abstraction, not toward
each other.
1# Good: OrderService depends on an injected abstraction;
2# MySqlOrderRepository depends on the same abstraction
3from abc import ABC, abstractmethod
4
5class OrderRepository(ABC):
6 @abstractmethod
7 def save(self, order: "Order") -> None: ...
8
9
10class MySqlOrderRepository(OrderRepository):
11 def save(self, order: "Order") -> None:
12 # connect and execute INSERT — details stay here
13 ...
14
15
16class InMemoryOrderRepository(OrderRepository):
17 """Swap-in for unit tests — no real DB needed."""
18 def __init__(self) -> None:
19 self._store: dict[str, "Order"] = {}
20
21 def save(self, order: "Order") -> None:
22 self._store[order.id] = order
23
24
25class OrderService:
26 def __init__(self, repo: OrderRepository) -> None:
27 self._repo = repo # injected; no new keyword, no import of MySQL
28
29 def place_order(self, order: "Order") -> None:
30 if order.total <= 0:
31 raise ValueError("Order total must be positive")
32 self._repo.save(order)