Skip to content
Plurafy

Dependency Injection

Dependency Injection (DI) decouples components and improves testability. Learn DI patterns and frameworks for managing dependencies in Node.js applications.

Implement Dependency Injection to create loosely coupled, testable, and maintainable applications.

Inversion of Control (IoC): Objects don’t create dependencies, they receive them Loose Coupling: Components depend on abstractions, not implementations Testability: Easy to mock dependencies for testing Maintainability: Changes to implementations don’t require changes to consumers

Constructor Injection: Dependencies passed via constructor Setter Injection: Dependencies set via methods Interface Injection: Dependencies implement specific interface

class UserService {
constructor(database, emailService, logger) {
this.database = database;
this.emailService = emailService;
this.logger = logger;
}
async createUser(userData) {
this.logger.info('Creating user', userData);
try {
const user = await this.database.users.create(userData);
await this.emailService.sendWelcomeEmail(user.email);
this.logger.info('User created successfully', { userId: user.id });
return user;
} catch (error) {
this.logger.error('Failed to create user', { error: error.message });
throw error;
}
}
async getUser(userId) {
return this.database.users.findById(userId);
}
}
// Dependency Injection Container
class Container {
constructor() {
this.services = new Map();
}
register(name, definition) {
this.services.set(name, definition);
}
get(name) {
const definition = this.services.get(name);
if (!definition) {
throw new Error(`Service ${name} not found`);
}
return typeof definition === 'function' ? definition() : definition;
}
}
// Setup
const container = new Container();
container.register('database', () => new Database());
container.register('emailService', () => new EmailService());
container.register('logger', () => console.log);
container.register('userService', () =>
new UserService(
container.get('database'),
container.get('emailService'),
container.get('logger')
)
);
const userService = container.get('userService');
const { Container, injectable, inject } = require('inversify');
// Define service identifiers
const TYPES = {
UserService: Symbol.for('UserService'),
UserRepository: Symbol.for('UserRepository'),
EmailService: Symbol.for('EmailService'),
Logger: Symbol.for('Logger'),
};
// Interfaces
class IUserRepository {
async findById(id) {}
async create(data) {}
}
class IEmailService {
async send(to, subject, body) {}
}
// Implementations
@injectable()
class UserRepository extends IUserRepository {
async findById(id) {
return { id, name: 'John Doe' };
}
async create(data) {
return { id: '1', ...data };
}
}
@injectable()
class EmailService extends IEmailService {
async send(to, subject, body) {
console.log(`Email sent to ${to}: ${subject}`);
}
}
@injectable()
class UserService {
constructor(
@inject(TYPES.UserRepository) userRepository,
@inject(TYPES.EmailService) emailService,
@inject(TYPES.Logger) logger
) {
this.userRepository = userRepository;
this.emailService = emailService;
this.logger = logger;
}
async createUser(userData) {
const user = await this.userRepository.create(userData);
await this.emailService.send(user.email, 'Welcome!', '...');
return user;
}
}
// Container setup
const container = new Container();
container.bind(TYPES.UserRepository).to(UserRepository).inSingletonScope();
container.bind(TYPES.EmailService).to(EmailService).inSingletonScope();
container.bind(TYPES.Logger).toConstantValue(console.log);
container.bind(TYPES.UserService).to(UserService);
const userService = container.get(TYPES.UserService);
class ServiceFactory {
static createUserService(config = {}) {
const database = new Database(config.dbUrl);
const emailService = new EmailService(config.emailProvider);
const logger = new Logger(config.logLevel);
return new UserService(database, emailService, logger);
}
static createOrderService(config = {}) {
const database = new Database(config.dbUrl);
const paymentService = new PaymentService(config.paymentProvider);
const notificationService = new NotificationService(config.notificationProvider);
return new OrderService(database, paymentService, notificationService);
}
}
// Usage
const userService = ServiceFactory.createUserService({
dbUrl: process.env.DATABASE_URL,
emailProvider: 'sendgrid',
logLevel: 'info',
});
// Mock implementations for testing
class MockUserRepository {
async findById(id) {
return { id: '1', name: 'Test User', email: 'test@example.com' };
}
async create(data) {
return { id: '1', ...data };
}
}
class MockEmailService {
async send(to, subject, body) {
// Mock implementation
console.log(`Mock email sent`);
}
}
// Test setup
const mockRepository = new MockUserRepository();
const mockEmailService = new MockEmailService();
const mockLogger = {
info: jest.fn(),
error: jest.fn(),
};
const userService = new UserService(
mockRepository,
mockEmailService,
mockLogger
);
// Test
describe('UserService', () => {
it('should create a user and send welcome email', async () => {
const user = await userService.createUser({
name: 'John',
email: 'john@example.com',
});
expect(user.name).toBe('John');
expect(mockLogger.info).toHaveBeenCalled();
});
});
services/userServiceModule.js
module.exports = (dependencies) => {
const { database, emailService, logger } = dependencies;
return {
async createUser(userData) {
logger.info('Creating user', userData);
const user = await database.users.create(userData);
await emailService.sendWelcomeEmail(user.email);
return user;
},
async getUser(userId) {
return database.users.findById(userId);
},
async updateUser(userId, updates) {
return database.users.update(userId, updates);
},
};
};
// app.js
const userServiceModule = require('./services/userServiceModule');
const dependencies = {
database: new Database(),
emailService: new EmailService(),
logger: console,
};
const userService = userServiceModule(dependencies);
function createAuthMiddleware(tokenService, userRepository) {
return async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const payload = tokenService.verify(token);
const user = await userRepository.findById(payload.userId);
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
req.user = user;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
};
}
// Setup
const app = express();
const tokenService = new TokenService();
const userRepository = new UserRepository();
app.use(createAuthMiddleware(tokenService, userRepository));
  1. Use Interfaces/Abstractions

    • Define contracts between services
    • Make mocking easier
    • Improve code organization
  2. Scope Management

    • Singleton: One instance shared
    • Transient: New instance each time
    • Scoped: One instance per request
  3. Configuration

    • Externalize configuration
    • Use environment variables
    • Support multiple environments
  4. Testing Strategy

    • Create mock implementations
    • Test in isolation
    • Use DI for test setup
  5. Documentation

    • Document service dependencies
    • Provide setup examples
    • Maintain dependency graph
  • Enterprise Applications: Complex dependency management
  • Microservices: Service discovery and registration
  • Testing Frameworks: Inject mocks and stubs
  • Plugin Systems: Dynamic service registration
  • Multi-tenant SaaS: Per-tenant dependency configuration