--- id: solid-dip-injection title: SOLID - Dependency Inversion (Injection) category: solid-principles priority: critical tags: [SOLID, DIP, dependency-injection, testability] related: [solid-dip-abstractions, solid-srp-class, core-composition] --- # Dependency Inversion Principle - Dependency Injection Dependencies should be injected from outside rather than created inside a class. This enables loose coupling, testability, and flexibility in how dependencies are provided. ## Bad Example ```typescript // Anti-pattern: Creating dependencies inside the class class UserRegistrationService { private userRepository: UserRepository; private passwordHasher: PasswordHasher; private emailService: EmailService; private logger: Logger; private analyticsService: AnalyticsService; constructor() { // Dependencies created inside - tight coupling this.userRepository = new PostgresUserRepository( new PostgresConnection('localhost', 5432, 'mydb') ); this.passwordHasher = new BcryptPasswordHasher(10); this.emailService = new SendGridEmailService(process.env.SENDGRID_KEY!); this.logger = new WinstonLogger('UserRegistration'); this.analyticsService = new MixpanelAnalytics(process.env.MIXPANEL_TOKEN!); } async register(email: string, password: string): Promise { this.logger.info(`Registering user: ${email}`); const existingUser = await this.userRepository.findByEmail(email); if (existingUser) { throw new Error('User already exists'); } const hashedPassword = await this.passwordHasher.hash(password); const user = await this.userRepository.create({ email, password: hashedPassword }); await this.emailService.send({ to: email, subject: 'Welcome!', body: 'Thanks for registering' }); await this.analyticsService.track('user_registered', { userId: user.id }); return user; } } // Problems: // 1. Cannot test without real Postgres, SendGrid, Mixpanel // 2. Cannot reuse with different implementations // 3. Hard-coded configuration scattered through constructors // 4. Class responsible for creating its dependencies ``` ## Good Example ```typescript // Correct approach: Dependencies injected from outside // Interfaces for all dependencies interface UserRepository { findByEmail(email: string): Promise; findById(id: string): Promise; create(data: CreateUserData): Promise; update(id: string, data: Partial): Promise; } interface PasswordHasher { hash(password: string): Promise; verify(password: string, hash: string): Promise; } interface EmailService { send(options: EmailOptions): Promise; } interface Logger { info(message: string, meta?: Record): void; error(message: string, error?: Error, meta?: Record): void; warn(message: string, meta?: Record): void; } interface AnalyticsService { track(event: string, properties?: Record): Promise; identify(userId: string, traits?: Record): Promise; } // Service with constructor injection class UserRegistrationService { constructor( private readonly userRepository: UserRepository, private readonly passwordHasher: PasswordHasher, private readonly emailService: EmailService, private readonly logger: Logger, private readonly analyticsService: AnalyticsService ) {} async register(email: string, password: string): Promise { this.logger.info('Registering user', { email }); const existingUser = await this.userRepository.findByEmail(email); if (existingUser) { throw new UserAlreadyExistsError(email); } const hashedPassword = await this.passwordHasher.hash(password); const user = await this.userRepository.create({ email, password: hashedPassword, createdAt: new Date() }); await this.sendWelcomeEmail(user); await this.trackRegistration(user); this.logger.info('User registered successfully', { userId: user.id }); return user; } private async sendWelcomeEmail(user: User): Promise { try { await this.emailService.send({ to: user.email, subject: 'Welcome!', body: 'Thanks for registering' }); } catch (error) { this.logger.error('Failed to send welcome email', error as Error, { userId: user.id }); // Don't fail registration if email fails } } private async trackRegistration(user: User): Promise { try { await this.analyticsService.identify(user.id, { email: user.email }); await this.analyticsService.track('user_registered', { userId: user.id }); } catch (error) { this.logger.error('Failed to track registration', error as Error, { userId: user.id }); // Don't fail registration if analytics fails } } } // Concrete implementations class PostgresUserRepository implements UserRepository { constructor(private db: DatabaseConnection) {} async findByEmail(email: string): Promise { const result = await this.db.query('SELECT * FROM users WHERE email = $1', [email]); return result.rows[0] || null; } async findById(id: string): Promise { const result = await this.db.query('SELECT * FROM users WHERE id = $1', [id]); return result.rows[0] || null; } async create(data: CreateUserData): Promise { const result = await this.db.query( 'INSERT INTO users (email, password, created_at) VALUES ($1, $2, $3) RETURNING *', [data.email, data.password, data.createdAt] ); return result.rows[0]; } async update(id: string, data: Partial): Promise { // Implementation } } class BcryptPasswordHasher implements PasswordHasher { constructor(private rounds: number = 10) {} async hash(password: string): Promise { return bcrypt.hash(password, this.rounds); } async verify(password: string, hash: string): Promise { return bcrypt.compare(password, hash); } } // Dependency Injection Container (manual) class Container { private services: Map = new Map(); register(key: string, factory: () => T): void { this.services.set(key, factory); } resolve(key: string): T { const factory = this.services.get(key); if (!factory) { throw new Error(`Service not registered: ${key}`); } return factory(); } } // Composition root - wire up all dependencies function configureContainer(): Container { const container = new Container(); // Register infrastructure container.register('DatabaseConnection', () => new PostgresConnection(process.env.DATABASE_URL!) ); container.register('Logger', () => new WinstonLogger({ level: process.env.LOG_LEVEL || 'info' }) ); // Register repositories container.register('UserRepository', () => new PostgresUserRepository(container.resolve('DatabaseConnection')) ); // Register services container.register('PasswordHasher', () => new BcryptPasswordHasher(12) ); container.register('EmailService', () => new SendGridEmailService(process.env.SENDGRID_API_KEY!) ); container.register('AnalyticsService', () => new MixpanelAnalytics(process.env.MIXPANEL_TOKEN!) ); // Register application services container.register('UserRegistrationService', () => new UserRegistrationService( container.resolve('UserRepository'), container.resolve('PasswordHasher'), container.resolve('EmailService'), container.resolve('Logger'), container.resolve('AnalyticsService') ) ); return container; } // Application startup const container = configureContainer(); const registrationService = container.resolve('UserRegistrationService'); // Testing is now trivial with mock implementations describe('UserRegistrationService', () => { let service: UserRegistrationService; let mockUserRepository: jest.Mocked; let mockPasswordHasher: jest.Mocked; let mockEmailService: jest.Mocked; let mockLogger: jest.Mocked; let mockAnalytics: jest.Mocked; beforeEach(() => { mockUserRepository = { findByEmail: jest.fn(), findById: jest.fn(), create: jest.fn(), update: jest.fn() }; mockPasswordHasher = { hash: jest.fn(), verify: jest.fn() }; mockEmailService = { send: jest.fn() }; mockLogger = { info: jest.fn(), error: jest.fn(), warn: jest.fn() }; mockAnalytics = { track: jest.fn(), identify: jest.fn() }; service = new UserRegistrationService( mockUserRepository, mockPasswordHasher, mockEmailService, mockLogger, mockAnalytics ); }); it('should register a new user', async () => { mockUserRepository.findByEmail.mockResolvedValue(null); mockPasswordHasher.hash.mockResolvedValue('hashed_password'); mockUserRepository.create.mockResolvedValue({ id: '1', email: 'test@example.com', password: 'hashed_password', createdAt: new Date() }); const user = await service.register('test@example.com', 'password123'); expect(user.id).toBe('1'); expect(mockPasswordHasher.hash).toHaveBeenCalledWith('password123'); expect(mockEmailService.send).toHaveBeenCalled(); expect(mockAnalytics.track).toHaveBeenCalledWith('user_registered', { userId: '1' }); }); it('should throw error if user already exists', async () => { mockUserRepository.findByEmail.mockResolvedValue({ id: '1', email: 'test@example.com' } as User); await expect(service.register('test@example.com', 'password123')) .rejects.toThrow(UserAlreadyExistsError); expect(mockUserRepository.create).not.toHaveBeenCalled(); }); it('should not fail registration if email sending fails', async () => { mockUserRepository.findByEmail.mockResolvedValue(null); mockPasswordHasher.hash.mockResolvedValue('hashed_password'); mockUserRepository.create.mockResolvedValue({ id: '1', email: 'test@example.com' } as User); mockEmailService.send.mockRejectedValue(new Error('SMTP error')); const user = await service.register('test@example.com', 'password123'); expect(user.id).toBe('1'); expect(mockLogger.error).toHaveBeenCalled(); }); }); ``` ## Why 1. **Testability**: All dependencies can be mocked. Tests run fast without external services. 2. **Flexibility**: Switch implementations at configuration time, not code change time. 3. **Single Responsibility**: Classes focus on business logic, not on constructing dependencies. 4. **Configuration Centralization**: All wiring happens in one place (composition root), making it easy to see and change. 5. **Lifetime Management**: The container can manage singleton vs. transient instances. 6. **Environment Adaptation**: Easy to provide different implementations for dev, test, staging, and production. 7. **Explicit Dependencies**: Looking at the constructor tells you exactly what a class needs to function.