Dependency Inversion Principle (DIP)
Depend on abstractions, not concrete implementations — both sides of a boundary point at an interface, not at each other.
Intent & Description
🎯 Intent
High-level business logic directly coupled to a specific database, API, or service can’t be tested, swapped, or evolved without changing the business code itself.
📋 Context
OrderProcessor instantiates MySQLDatabase directly. You can’t test OrderProcessor without a real database. Switching to MongoDB means editing the business logic class. The high-level policy is at the mercy of a low-level implementation detail.
💡 Solution
Define an interface (Database). OrderProcessor depends on the interface. MySQLDatabase and MongoDBDatabase both implement it. The concrete implementation is injected at runtime. Now both layers point at the abstraction — neither layer knows the other exists.
Real-world Use Case
📌 TL;DR
Depend on interfaces, not implementations. Decouples layers, enables mocking, and makes the codebase resilient to low-level change. The foundation of dependency injection.
Advantages
- Loose coupling — swap any implementation without touching the caller
- Easy to test with mocks or stubs injected at any boundary
- High-level business logic is insulated from low-level churn
Disadvantages
- More indirection — every dependency adds an interface and an injection point
- Requires upfront abstraction design — wrong abstractions are expensive to reverse
- Overkill for simple scripts or throwaway code with no testing requirement
// Before: Direct dependency on concretion
class OrderProcessor {
constructor() {
this.database = new MySQLDatabase(); // Direct dependency
}
processOrder(order) {
this.database.save(order);
}
}
// After: DIP applied
class Database {
save(data) {
throw new Error('Must implement save');
}
}
class MySQLDatabase extends Database {
save(data) {
console.log(`Saving to MySQL: ${data}`);
}
}
class MongoDBDatabase extends Database {
save(data) {
console.log(`Saving to MongoDB: ${data}`);
}
}
class OrderProcessor {
constructor(database) {
this.database = database; // Depends on abstraction
}
processOrder(order) {
this.database.save(order);
}
}
// Can easily swap implementations
const processor1 = new OrderProcessor(new MySQLDatabase());
const processor2 = new OrderProcessor(new MongoDBDatabase());