Dependency Inversion Principle (DIP)
"High-level modules should not depend on low-level modules. Both should depend on abstractions."
The Dependency Inversion Principle states that we should depend on interfaces or abstract classes instead of concrete implementations. This makes our code more flexible and easier to test.
The Problem: Violating DIP
// ❌ BAD: High-level module depends on low-level module
class MySQLDatabase {
connect(): void {
console.log("Connected to MySQL");
}
query(sql: string): any[] {
console.log(`MySQL query: ${sql}`);
return [];
}
}
// High-level module
class UserService {
private database: MySQLDatabase; // Direct dependency on concrete class
constructor() {
this.database = new MySQLDatabase(); // Tightly coupled!
}
getUser(id: string): any {
this.database.connect();
return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
// Problems:
// 1. UserService is tightly coupled to MySQLDatabase
// 2. Can't switch to PostgreSQL without modifying UserService
// 3. Hard to test UserService (requires actual MySQL)
// 4. Changes to MySQLDatabase affect UserServiceThe Solution: Applying DIP
// ✅ GOOD: Both depend on abstraction
// Abstraction (interface)
interface Database {
connect(): void;
query(sql: string): any[];
}
// Low-level modules implement the abstraction
class MySQLDatabase implements Database {
connect(): void {
console.log("Connected to MySQL");
}
query(sql: string): any[] {
console.log(`MySQL query: ${sql}`);
return [];
}
}
class PostgreSQLDatabase implements Database {
connect(): void {
console.log("Connected to PostgreSQL");
}
query(sql: string): any[] {
console.log(`PostgreSQL query: ${sql}`);
return [];
}
}
class MongoDatabase implements Database {
connect(): void {
console.log("Connected to MongoDB");
}
query(sql: string): any[] {
console.log(`MongoDB query: ${sql}`);
return [];
}
}
// High-level module depends on abstraction
class UserService {
constructor(private database: Database) {} // Dependency injection
getUser(id: string): any {
this.database.connect();
return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
}
getAllUsers(): any[] {
this.database.connect();
return this.database.query(`SELECT * FROM users`);
}
}
// Usage - easy to switch implementations
const mysqlService = new UserService(new MySQLDatabase());
const pgService = new UserService(new PostgreSQLDatabase());
const mongoService = new UserService(new MongoDatabase());
// Easy to test with mock
class MockDatabase implements Database {
connect(): void {}
query(sql: string): any[] {
return [{ id: '1', name: 'Test User' }];
}
}
const testService = new UserService(new MockDatabase());Real-World Example: Notification System
// ❌ BAD: Direct dependencies
class EmailSender {
send(to: string, message: string): void {
console.log(`Email sent to ${to}: ${message}`);
}
}
class UserRegistration {
private emailSender: EmailSender;
constructor() {
this.emailSender = new EmailSender(); // Tight coupling
}
register(email: string, name: string): void {
// Registration logic
this.emailSender.send(email, `Welcome ${name}!`);
}
}
// ✅ GOOD: Depend on abstraction
interface NotificationService {
send(to: string, message: string): void;
}
class EmailNotification implements NotificationService {
send(to: string, message: string): void {
console.log(`📧 Email to ${to}: ${message}`);
// SMTP logic here
}
}
class SMSNotification implements NotificationService {
send(to: string, message: string): void {
console.log(`📱 SMS to ${to}: ${message}`);
// SMS API logic here
}
}
class PushNotification implements NotificationService {
send(to: string, message: string): void {
console.log(`🔔 Push to ${to}: ${message}`);
// Push notification logic here
}
}
class SlackNotification implements NotificationService {
send(to: string, message: string): void {
console.log(`💬 Slack to ${to}: ${message}`);
// Slack API logic here
}
}
// High-level module
class UserRegistration {
constructor(private notificationService: NotificationService) {}
register(identifier: string, name: string): void {
console.log(`Registering user: ${name}`);
// Registration logic...
this.notificationService.send(
identifier,
`Welcome ${name}! Your account has been created.`
);
}
}
// Flexible usage
const emailReg = new UserRegistration(new EmailNotification());
emailReg.register('user@example.com', 'John');
const smsReg = new UserRegistration(new SMSNotification());
smsReg.register('+1234567890', 'Jane');
const pushReg = new UserRegistration(new PushNotification());
pushReg.register('device-123', 'Bob');
// Multi-channel notification
class MultiChannelNotification implements NotificationService {
constructor(private services: NotificationService[]) {}
send(to: string, message: string): void {
this.services.forEach(service => service.send(to, message));
}
}
const multiChannel = new UserRegistration(
new MultiChannelNotification([
new EmailNotification(),
new PushNotification()
])
);Dependency Injection Patterns
// Three types of dependency injection
// 1. Constructor Injection (Recommended)
class OrderService {
constructor(
private database: Database,
private emailService: NotificationService,
private paymentProcessor: PaymentProcessor
) {}
placeOrder(order: Order): void {
this.database.query('INSERT INTO orders...');
this.paymentProcessor.process(order);
this.emailService.send(order.email, 'Order confirmed');
}
}
// 2. Property Injection
class OrderService {
database!: Database;
emailService!: NotificationService;
setDatabase(db: Database): void {
this.database = db;
}
setEmailService(service: NotificationService): void {
this.emailService = service;
}
}
// 3. Method Injection
class OrderService {
placeOrder(
order: Order,
database: Database,
emailService: NotificationService
): void {
database.query('INSERT INTO orders...');
emailService.send(order.email, 'Order confirmed');
}
}
// Dependency Injection Container (Simple Example)
class Container {
private services: Map<string, any> = new Map();
register<T>(name: string, implementation: new (...args: any[]) => T): void {
this.services.set(name, implementation);
}
resolve<T>(name: string): T {
const Service = this.services.get(name);
if (!Service) {
throw new Error(`Service ${name} not found`);
}
return new Service();
}
}
// Usage
const container = new Container();
container.register('database', MySQLDatabase);
container.register('notification', EmailNotification);
const db = container.resolve<Database>('database');
const notifier = container.resolve<NotificationService>('notification');
const orderService = new OrderService(db, notifier, paymentProcessor);Testing with DIP
// Easy to test with mocks
interface UserRepository {
findById(id: string): User | null;
save(user: User): void;
}
interface EmailService {
send(to: string, subject: string, body: string): void;
}
class UserService {
constructor(
private userRepo: UserRepository,
private emailService: EmailService
) {}
resetPassword(userId: string): void {
const user = this.userRepo.findById(userId);
if (!user) {
throw new Error('User not found');
}
const newPassword = this.generatePassword();
user.password = newPassword;
this.userRepo.save(user);
this.emailService.send(
user.email,
'Password Reset',
`Your new password is: ${newPassword}`
);
}
private generatePassword(): string {
return Math.random().toString(36).slice(-8);
}
}
// Mock implementations for testing
class MockUserRepository implements UserRepository {
private users: Map<string, User> = new Map();
findById(id: string): User | null {
return this.users.get(id) || null;
}
save(user: User): void {
this.users.set(user.id, user);
}
addUser(user: User): void {
this.users.set(user.id, user);
}
}
class MockEmailService implements EmailService {
sentEmails: Array<{ to: string; subject: string; body: string }> = [];
send(to: string, subject: string, body: string): void {
this.sentEmails.push({ to, subject, body });
}
}
// Test
function testPasswordReset(): void {
const mockRepo = new MockUserRepository();
const mockEmail = new MockEmailService();
const userService = new UserService(mockRepo, mockEmail);
// Setup test data
mockRepo.addUser({
id: '1',
email: 'test@example.com',
password: 'old-password'
});
// Execute
userService.resetPassword('1');
// Verify
console.assert(mockEmail.sentEmails.length === 1, 'Email should be sent');
console.assert(
mockEmail.sentEmails[0].to === 'test@example.com',
'Email sent to correct address'
);
console.log('✓ Test passed');
}
testPasswordReset();Benefits of DIP
Loose coupling: Modules are independent of concrete implementations
Easy testing: Can inject mocks and stubs for unit tests
Flexibility: Easy to swap implementations without changing high-level code
Maintainability: Changes to low-level modules don't affect high-level modules
Parallel development: Teams can work on different modules independently
SOLID Summary
All five SOLID principles work together to create maintainable, flexible, and testable code:
Single Responsibility: One class, one job
Open/Closed: Open for extension, closed for modification
Liskov Substitution: Subclasses must be substitutable
Interface Segregation: Many specific interfaces better than one general
Dependency Inversion: Depend on abstractions, not concretions
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free