Facebook Pixel

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)
Invest in Yourself
Your new job is waiting. 83% of people that complete the program get a job offer. Unlock unlimited access to all content and features.
Go Pro