Skip to main content

Command Palette

Search for a command to run...

Dependency Injection: Decouple Code Dependencies

Learn: Dependency Injection: Decouple Code Dependencies

Updated
6 min readView as Markdown
T

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:

  1. Constructor Injection - Dependencies passed via constructor
  2. Setter Injection - Dependencies set via methods
  3. 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

// ✅ 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

AspectWithout DIWith DI
CouplingTightLoose
TestabilityHardEasy
FlexibilityLowHigh
MaintenanceDifficultSimple
ReusabilityLimitedExcellent
ConfigurationHardcodedCentralized

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)