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 can make policy tests easier to isolate. Other seams and integration tests remain useful; dependency inversion is not a prerequisite for all serious testing.
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. Physical proximity alone is not a violation. Check source dependencies, ownership and whether infrastructure types leak into the contract.
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.
Read This Only If Stuck
- Clean Code: Dependency inversion and systems
- Good Code, Bad Code: Design code with dependency injection in mind
- Head First Design Patterns: Design principle challenge
Engineering extension: Injection is wiring; inversion is ownership
Passing a concrete database object through a constructor is injection even if the policy still imports database-specific rows and errors. To invert that dependency, specify the policy capability without infrastructure types and adapt at the edge. A callable can implement a small capability in Python; a container is optional. Try: inject a rate function returning a Money value. Check: source dependencies and failure contracts matter even when replacing the collaborator in a test is easy.
Use the design contracts and practice feedback after an independent attempt.