LESSON 2.5
Dependency inversion
High-level policy should not import low-level detail. Both should depend on an abstraction that neither owns.
7 min read
An OrderService that calls new MySQLOrderRepository() is locked to MySQL. Switching to PostgreSQL or adding a test double requires editing the service. The high-level policy — recording an order — should not care which database stores it.
The problem
class MySQLOrderRepository {
save(order) { return `MySQL: saved ${order.id}`; }
}
class OrderService {
#repo = new MySQLOrderRepository(); // Hard-wired to MySQL.
placeOrder(order) {
return this.#repo.save(order);
}
}
// To test OrderService without MySQL, you must stub the class constructor.
// To switch to Postgres, you must edit OrderService.Every call to placeOrder depends on the MySQL driver being available and configured. Unit tests need a running database. Swapping databases requires touching business logic.
The rule
Dependency inversion principle: high-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.
In practice: the OrderService declares the interface OrderRepository it needs. MySQLOrderRepository implements that interface. OrderService receives a concrete instance through its constructor, never creating one itself.
The word inversion refers to the classical dependency direction: instead of OrderService → MySQLOrderRepository, the detail depends on the abstraction: MySQLOrderRepository → OrderRepository ← OrderService. The abstraction is owned by the module that uses it, not the module that implements it.
In code
DIP in practice
DIP is often called dependency injection in everyday conversation. They are related but different: DIP is the principle (depend on abstractions, not concretions). Dependency injection is one mechanism for achieving it (pass the concrete instance in from the outside). A DI framework is one tool for wiring the graph of objects; DIP applies just as well without a framework, using plain constructors.
The cost: one interface per external dependency. That is a fixed, small cost. The return: the high-level class becomes testable without starting any infrastructure.
Where you have already seen it
Express middleware receives req, res and next — abstractions. It never constructs its own HTTP socket. Java's JDBC Connection is an interface; DriverManager.getConnection() returns a concrete driver. React's useState is an abstraction over the renderer; the same hook works with ReactDOM and React Native.
Try it
Dependency Injection Container LLD
Wire a real and a test repository to the same service using constructor injection and measure the difference in test setup time.