SOLID Principles: Dependency Inversion Principle (DIP)
SOLID Principles: Dependency Inversion Principle (DIP)
The Dependency Inversion Principle (DIP) is the D in SOLID:
- 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.
The word "inversion" doesn't mean flip the dependency. It means abstractions control the direction — high-level modules define what they need, and low-level implementations conform to those needs.
- What Is the Dependency Inversion Principle
- The Problem with Direct Dependencies
- Depend on Abstractions
- Dependency Injection
- Summary
What Is the Dependency Inversion Principle
Consider an order service that needs a database connection, an email sender, and a logger. If the service creates those dependencies directly, it's what DIP calls a high-level module depending on low-level modules.
High-level module: OrderService — owns the business logic Low-level modules: database, email, logger — handle implementation details
DIP says: neither should depend directly on the other. Both should depend on abstractions.
The Problem with Direct Dependencies
class MySQLOrderRepository {
save(order: Order): void {
mysql.query(`INSERT INTO orders ...`);
}
}
class SMTPNotifier {
send(to: string, message: string): void {
smtp.sendMail({ to, text: message });
}
}
// high-level module directly depending on low-level implementations
class OrderService {
private repo = new MySQLOrderRepository(); // tightly coupled
private notifier = new SMTPNotifier(); // tightly coupled
placeOrder(userId: string, items: Item[]): void {
const order = buildOrder(userId, items);
this.repo.save(order);
this.notifier.send(userId, 'Order placed successfully');
}
}Problems:
- Switching from MySQL to PostgreSQL means changing
OrderService - Testing
OrderServicerequires a real MySQL connection and SMTP server OrderService→ concrete class: high-level depends on low-level
Depend on Abstractions
Replace the concrete dependencies with interfaces that the high-level module defines:
// high-level module defines what it needs
interface OrderRepository {
save(order: Order): void;
}
interface Notifier {
send(to: string, message: string): void;
}
// low-level modules implement the abstractions
class MySQLOrderRepository implements OrderRepository {
save(order: Order): void {
mysql.query(`INSERT INTO orders ...`);
}
}
class SMTPNotifier implements Notifier {
send(to: string, message: string): void {
smtp.sendMail({ to, text: message });
}
}
// high-level module depends on abstractions, not implementations
class OrderService {
constructor(
private repo: OrderRepository, // interface, not concrete class
private notifier: Notifier, // interface, not concrete class
) {}
placeOrder(userId: string, items: Item[]): void {
const order = buildOrder(userId, items);
this.repo.save(order);
this.notifier.send(userId, 'Order placed successfully');
}
}Now OrderService and MySQLOrderRepository both depend on OrderRepository — they no longer depend directly on each other.
Dependency Injection
DIP is usually implemented through dependency injection (DI) — dependencies are passed in from outside rather than created internally:
// wire up the concrete implementations
const repo = new MySQLOrderRepository();
const notifier = new SMTPNotifier();
// inject dependencies into OrderService
const orderService = new OrderService(repo, notifier);In tests, inject mocks instead:
const mockRepo: OrderRepository = {
save: jest.fn(),
};
const mockNotifier: Notifier = {
send: jest.fn(),
};
// no real database or SMTP needed — fully controlled
const orderService = new OrderService(mockRepo, mockNotifier);Tests are isolated, deterministic, and fast. Swapping implementations in production is equally clean.
Summary
DIP in practice:
- High-level modules define the abstractions they depend on; low-level implementations conform to them
- Abstractions act as a firewall between high-level logic and low-level details
- Both layers can evolve and be tested independently
DIP is the foundation for dependency injection containers, service locators, and other advanced inversion-of-control patterns. All five SOLID principles are now complete — independent, but each one reinforcing the others.