Dependency Injection: Decouple Code Dependencies
Learn: Dependency Injection: Decouple Code Dependencies
Welcome to TopperBlog! 👋
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
🎯 What I Write About:
• AI/ML Engineering & LLMs
• Web3 & Blockchain Development
• System Design & Architecture
• Interview Preparation (FAANG)
• Freelancing & Remote Work
• Modern Tech Stacks (Next.js, React, Rust, TypeScript)
• Performance Optimization & Best Practices
💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
🌐 Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
Dependency Injection: Decouple Code Dependencies
Problem
Tight Coupling Issues
// ❌ TIGHTLY COUPLED CODE
class UserService {
constructor() {
this.database = new MySQLDatabase();
this.emailService = new GmailEmailService();
this.logger = new FileLogger();
}
registerUser(email, password) {
this.logger.log("Registering user: " + email);
const user = this.database.save({ email, password });
this.emailService.send(email, "Welcome!");
return user;
}
}
// Problems:
// 1. Hard to test - can't mock dependencies
// 2. Difficult to swap implementations (PostgreSQL instead of MySQL)
// 3. Changes in dependencies require code modifications
// 4. Violates Single Responsibility Principle
// 5. Difficult to reuse UserService with different configurations
Testing Nightmare
// ❌ IMPOSSIBLE TO TEST PROPERLY
describe("UserService", () => {
it("should register user", () => {
const service = new UserService();
// This actually sends real emails and writes to real database!
service.registerUser("test@example.com", "password");
});
});
Solution
Dependency Injection Pattern
Core Concept: Pass dependencies to a class rather than having it create them internally.
Three Types of DI:
- Constructor Injection - Dependencies passed via constructor
- Setter Injection - Dependencies set via methods
- Interface Injection - Dependencies injected through interfaces
Benefits
- ✅ Testability - Easy to inject mock dependencies
- ✅ Flexibility - Swap implementations without code changes
- ✅ Maintainability - Clear dependency graph
- ✅ Reusability - Same class works with different configurations
- ✅ Loose Coupling - Classes depend on abstractions, not concrete implementations
Code Examples
1. Constructor Injection (Recommended)
// ✅ LOOSELY COUPLED WITH CONSTRUCTOR INJECTION
// Define interfaces/contracts
class Database {
save(data) { throw new Error("Not implemented"); }
}
class EmailService {
send(to, subject, body) { throw new Error("Not implemented"); }
}
class Logger {
log(message) { throw new Error("Not implemented"); }
}
// Concrete implementations
class MySQLDatabase extends Database {
save(data) {
console.log("Saving to MySQL:", data);
return { id: 1, ...data };
}
}
class PostgresDatabase extends Database {
save(data) {
console.log("Saving to PostgreSQL:", data);
return { id: 1, ...data };
}
}
class GmailEmailService extends EmailService {
send(to, subject, body) {
console.log(`Sending email via Gmail to ${to}`);
}
}
class ConsoleLogger extends Logger {
log(message) {
console.log(`[LOG] ${message}`);
}
}
// Service with injected dependencies
class UserService {
constructor(database, emailService, logger) {
this.database = database;
this.emailService = emailService;
this.logger = logger;
}
registerUser(email, password) {
this.logger.log(`Registering user: ${email}`);
const user = this.database.save({ email, password });
this.emailService.send(email, "Welcome!", "Welcome to our platform!");
return user;
}
}
// Usage
const database = new MySQLDatabase();
const emailService = new GmailEmailService();
const logger = new ConsoleLogger();
const userService = new UserService(database, emailService, logger);
userService.registerUser("john@example.com", "secure123");
2. Easy Testing with Mocks
// ✅ SIMPLE TESTING WITH MOCK DEPENDENCIES
class MockDatabase extends Database {
constructor() {
super();
this.savedData = [];
}
save(data) {
this.savedData.push(data);
return { id: 1, ...data };
}
}
class MockEmailService extends EmailService {
constructor() {
super();
this.sentEmails = [];
}
send(to, subject, body) {
this.sentEmails.push({ to, subject, body });
}
}
class MockLogger extends Logger {
constructor() {
super();
this.logs = [];
}
log(message) {
this.logs.push(message);
}
}
// Test
describe("UserService", () => {
it("should register user and send welcome email", () => {
const mockDb = new MockDatabase();
const mockEmail = new MockEmailService();
const mockLogger = new MockLogger();
const service = new UserService(mockDb, mockEmail, mockLogger);
const user = service.registerUser("test@example.com", "pass123");
expect(user.email).toBe("test@example.com");
expect(mockDb.savedData.length).toBe(1);
expect(mockEmail.sentEmails.length).toBe(1);
expect(mockLogger.logs.length).toBe(1);
});
it("should work with different database implementation", () => {
const postgresDb = new PostgresDatabase();
const mockEmail = new MockEmailService();
const mockLogger = new MockLogger();
const service = new UserService(postgresDb, mockEmail, mockLogger);
const user = service.registerUser("test@example.com", "pass123");
expect(user.email).toBe("test@example.com");
});
});
3. IoC Container (Dependency Injection Framework)
// ✅ INVERSION OF CONTROL CONTAINER
class Container {
constructor() {
this.services = {};
this.singletons = {};
}
// Register a service factory
register(name, factory, isSingleton = false) {
this.services[name] = { factory, isSingleton };
}
// Resolve a service
resolve(name) {
if (!this.services[name]) {
throw new Error(`Service '${name}' not registered`);
}
const { factory, isSingleton } = this.services[name];
// Return singleton if already created
if (isSingleton && this.singletons[name]) {
return this.singletons[name];
}
// Create new instance
const instance = factory(this);
// Cache singleton
if (isSingleton) {
this.singletons[name] = instance;
}
return instance;
}
}
// Setup container
const container = new Container();
// Register services
container.register("database", () => new MySQLDatabase(), true);
container.register("emailService", () => new GmailEmailService(), true);
container.register("logger", () => new ConsoleLogger(), true);
container.register("userService", (c) => {
return new UserService(
c.resolve("database"),
c.resolve("emailService"),
c.resolve("logger")
);
});
// Usage
const userService = container.resolve("userService");
userService.registerUser("alice@example.com", "secure456");
// Easy to swap implementations
container.register("database", () => new PostgresDatabase(), true);
const userService2 = container.resolve("userService");
userService2.registerUser("bob@example.com", "secure789");
4. Advanced IoC Container with Auto-Wiring
// ✅ SMART CONTAINER WITH AUTOMATIC DEPENDENCY RESOLUTION
class SmartContainer {
constructor() {
this.services = new Map();
this.singletons = new Map();
}
register(name, Class, isSingleton = false) {
this.services.set(name, { Class, isSingleton });
}
resolve(name) {
if (!this.services.has(name)) {
throw new Error(`Service '${name}' not registered`);
}
if (this.singletons.has(name)) {
return this.singletons.get(name);
}
const { Class, isSingleton } = this.services.get(name);
const instance = this._instantiate(Class);
if (isSingleton) {
this.singletons.set(name, instance);
}
return instance;
}
_instantiate(Class) {
const params = this._getConstructorParams(Class);
const dependencies = params.map((param) => this.resolve(param));
return new Class(...dependencies);
}
_getConstructorParams(Class) {
const funcStr = Class.toString();
const match = funcStr.match(/constructor\s*\(\s*([^)]*)\s*\)/);
if (!match) return [];
return match[1]
.split(",")
.map((param) => param.trim())
.filter((param) => param);
}
}
// Usage with auto-wiring
const container = new SmartContainer();
container.register("database", MySQLDatabase, true);
container.register("emailService", GmailEmailService, true);
container.register("logger", ConsoleLogger, true);
container.register("userService", UserService);
// Automatically resolves dependencies!
const userService = container.resolve("userService");
userService.registerUser("charlie@example.com", "secure999");
5. Real-World Example: Express.js with DI
// ✅ DEPENDENCY INJECTION IN EXPRESS APPLICATION
const express = require("express");
class UserRepository {
async findById(id) {
return { id, name: "John Doe", email: "john@example.com" };
}
async save(user) {
return { id: 1, ...user };
}
}
class UserController {
constructor(userRepository, logger) {
this.userRepository = userRepository;
this.logger = logger;
}
async getUser(req, res) {
try {
this.logger.log(`Fetching user ${req.params.id}`);
const user = await this.userRepository.findById(req.params.id);
res.json(user);
} catch (error) {
this.logger.log(`Error: ${error.message}`);
res.status(500).json({ error: error.message });
}
}
async createUser(req, res) {
try {
this.logger.log(`Creating user: ${req.body.email}`);
const user = await this.userRepository.save(req.body);
res.status(201).json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
}
// Setup
const app = express();
app.use(express.json());
const container = new SmartContainer();
container.register("userRepository", UserRepository, true);
container.register("logger", ConsoleLogger, true);
container.register("userController", UserController);
const userController = container.resolve("userController");
// Routes
app.get("/users/:id", (req, res) => userController.getUser(req, res));
app.post("/users", (req, res) => userController.createUser(req, res));
app.listen(3000, () => console.log("Server running on port 3000"));
Key Takeaways
| Aspect | Without DI | With DI |
| Coupling | Tight | Loose |
| Testability | Hard | Easy |
| Flexibility | Low | High |
| Maintenance | Difficult | Simple |
| Reusability | Limited | Excellent |
| Configuration | Hardcoded | Centralized |
Best Practices:
- Inject dependencies through constructor
- Depend on abstractions, not concrete classes
- Use IoC containers for complex applications
- Keep container configuration centralized
- Avoid service locator pattern (anti-pattern)