Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Problem
UserController is hardwired to FileLogger. Swapping to a console or mock logger requires changing the controller:
// ❌ High-level depends on low-level concrete
class FileLogger {
log(msg: string) { /* write to file */ }
}
class UserController {
constructor() { this.logger = new FileLogger(); } // tightly coupled
save(user: { name: string }) { this.logger.log(`Saved ${user.name}`); }
}
Solution
Depend on a Logger abstraction; inject the implementation:
interface Logger {
log(msg: string): void;
}
class ConsoleLogger implements Logger {
log(msg: string) { console.log(msg); }
}
class UserController {
constructor(private logger: Logger) {}
save(user: { name: string }) { this.logger.log(`Saved ${user.name}`); }
}
// Wiring — swap implementations without touching UserController:
const controller = new UserController(new ConsoleLogger());
// const fileController = new UserController(new FileLogger());
// const testController = new UserController(new MockLogger());
Architecture Diagram
┌─────────────────┐
│ UserController │ ← High-level
└────────┬────────┘
│ depends on abstraction
↓
┌──────────────────────┐
│ Logger (interface) │ ← Abstraction
└────────┬─────────────┘
↑ implements
┌─────────────────┐
│ ConsoleLogger │ ← Low-level
└─────────────────┘
Key Insight
Without DIP, swapping a low-level detail (e.g. database vendor, email provider) ripples through your entire codebase. DIP isolates the blast radius to a single wiring point.
Real-world Examples
EmailServiceinterface →SendGridEmailService,SESEmailServiceFileStorageinterface →S3Storage,AzureBlobStoragePaymentProcessorinterface →StripeProcessor,PayPalProcessor