Dependency Inversion: Depend on Abstractions, Not Concretions
What This Concept Is
The Dependency Inversion Principle (DIP) has two parts:
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.
"High-level" modules hold policy -- business rules, workflow, domain logic. "Low-level" modules hold mechanism -- databases, HTTP clients, file systems. Without inversion, policy imports mechanism. With inversion, both import a shared abstraction, and mechanism plugs in behind it.
DIP is not "use dependency injection." It is a claim about which way the arrows point in the dependency graph. Dependency injection is one common way to honor DIP, but the principle is about direction.
Why It Matters Here
DIP is the principle that lets a codebase replace its infrastructure without rewriting its business logic. It is the precondition for serious testability: if policy depends directly on a real database, there is no seam to put a fake behind.
It also explains why an interface is worth introducing. An interface pays for itself when it protects stable policy from volatile details.
Concrete Example
DIP violation -- OrderService (policy) imports PostgresOrderDb (detail):
import { PostgresOrderDb } from "./infra/postgres";
class OrderService {
private db = new PostgresOrderDb();
place(order: Order) {
if (order.total() > 0) this.db.insert(order);
}
}
The arrow points OrderService -> PostgresOrderDb. To swap Postgres for DynamoDB, tests, or a stub, you must edit OrderService.
DIP-respecting version -- both depend on the abstraction:
interface OrderRepository { insert(order: Order): void; }
class OrderService {
constructor(private repo: OrderRepository) {}
place(order: Order) {
if (order.total() > 0) this.repo.insert(order);
}
}
class PostgresOrderRepository implements OrderRepository { /* ... */ }
class InMemoryOrderRepository implements OrderRepository { /* tests */ }
The arrow now points OrderService -> OrderRepository <- PostgresOrderRepository. Policy no longer knows which database exists; details depend on the abstraction policy owns.
Common Confusion / Misconception
"DIP means using a DI container." Not really. A DI container is one mechanism for assembling the object graph; DIP is about which modules depend on which. You can honor DIP with plain constructor arguments and no framework.
A second misconception: "the interface should live with its implementation." Under DIP, the interface typically belongs to the policy side -- the package that needs a capability owns the abstraction, and the provider plugs in. Putting the interface next to the implementation is a common subtle violation.
A third: "everything should have an interface." DIP is about protecting policy from volatile details. A stable value class with no variation does not need an interface.
How To Use It
When you see a direct dependency from policy to detail:
- Identify which side owns the policy (high-level module).
- Name the capability policy needs (e.g.,
OrderRepository,Clock,EmailSender). - Declare the interface in policy's package.
- Make the existing implementation depend on that interface.
- Assemble the object graph at a "main" seam -- where the program starts -- rather than inside policy.
Check Yourself
- Why does DIP say "abstractions should not depend on details" separately from the first claim?
- Why is owning the interface on the policy side the usual correct choice?
- Why is "every class has an interface" not the same thing as applying DIP?
Mini Drill or Application
Pick a module that directly imports a database, HTTP client, or file-system library. Do all four:
- Identify the capability policy needs from that dependency (insert, fetch, send, read).
- Write a minimal interface for that capability.
- Refactor mentally: where does the interface live, where does the implementation live, and what assembles them?
- Describe how the same policy would be tested with an in-memory implementation.