Dependency Inversion Principle
High-level modules should not depend on low-level modules — both should depend on abstractions, inverting the conventional dependency direction.
★★★★★4/5Inside a codebase — classes, modules, files
How it works
The Dependency Inversion Principle (DIP) is the 'D' in SOLID (Robert C. Martin, 1994). It has two statements: (1) high-level modules should not depend on low-level modules — both should depend on abstractions; (2) abstractions should not depend on details — details should depend on abstractions.
In a conventional layered design, the OrderService (high-level) depends on PostgresDatabase (low-level) directly. If the database changes, the service must change. DIP inverts this: define an OrderRepository interface, have the service depend on the interface, and have PostgresDatabase implement it. Now the high-level module owns the interface — the low-level module depends on the high-level abstraction, not the other way around.
This is the principle that underlies Dependency Injection, Hexagonal Architecture, and the Repository pattern. DIP is about ownership of the abstraction: the interface belongs to the high-level policy layer, not the low-level detail layer.
Implementation
TypeScript · Go · Rust// ❌ High-level OrderService directly depends on low-level PostgresDB
class PostgresDB {
save(order: Order): void { /* SQL INSERT */ }
}
class OrderService {
private db = new PostgresDB(); // ❌ concrete dependency — impossible to test or swap
placeOrder(order: Order): void {
this.db.save(order);
}
}
// ✓ Both depend on an abstraction — the interface belongs to the high-level layer
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
class OrderService {
constructor(private repo: OrderRepository) {} // depends on abstraction ✓
async placeOrder(order: Order): Promise<void> {
await this.repo.save(order);
}
}
// Low-level module implements the high-level abstraction
class PostgresOrderRepository implements OrderRepository {
async save(order: Order) { /* SQL INSERT */ }
async findById(id: string) { /* SQL SELECT */ return null; }
}
// Test double — zero infrastructure required
class InMemoryOrderRepository implements OrderRepository {
private orders = new Map<string, Order>();
async save(o: Order) { this.orders.set(o.id, o); }
async findById(id: string) { return this.orders.get(id) ?? null; }
}Why it matters
High-level business logic is the most valuable code in a system. When it directly imports low-level infrastructure (databases, HTTP clients, email providers), it becomes fragile and hard to test. DIP keeps the valuable code stable by letting infrastructure adapt to it, not the other way around.
✓ When to use
- →Any time a high-level module interacts with infrastructure (database, email, API, queue)
- →When you want to unit-test business logic without spinning up infrastructure
- →When the infrastructure provider might change (moving from AWS to GCP, MySQL to Postgres)
- →In combination with DI containers that resolve abstractions to implementations
✗ When NOT to use
- →Low-level utility code (parsers, formatters) where no abstraction is needed
- →When the dependency is stable and will never change (standard library functions)
Trade-offs
High-level policy is decoupled from low-level details — each changes independently
Requires defining and maintaining interfaces — more files, more surface area
Business logic can be tested without real infrastructure
Misapplied DIP adds interfaces where none are needed, increasing complexity
Infrastructure can be replaced transparently
Requires a wiring layer (DI container or manual factory) to connect abstractions to implementations
In production
Every Spring bean is accessed through an interface; the container inverts control and resolves dependencies at startup
Providers registered by interface token — @Inject() receives an abstraction; the module wires the concrete class
Use case layer defines port interfaces; infrastructure layer implements them — dependency arrows always point inward
Industry adoption
Related principles
Dependency Injection
Supply a component's dependencies from the outside rather than letting it construct them — so implementations can be swapped without changing the component.
Interface / Contract
Define what a component must do without dictating how it does it — so implementations can vary freely while callers remain stable.
Interface Segregation Principle
Prefer many small, focused interfaces over one large general-purpose one — clients should not be forced to depend on methods they don't use.
Hexagonal Architecture
Place the domain at the centre and connect all I/O (HTTP, database, messaging) through explicit ports and adapters — so the core is framework-free and infinitely testable.