6 minutes tech - Dependency Injection and IOC

6 minutes tech - Dependency Injection and IOC

Post Date : 2025-10-11T13:11:49+07:00

Modified Date : 2025-10-11T13:11:49+07:00

Category: cheatsheet 6minutes-tech

Tags:

Dependency injection

Let’s take a look at this example

class Club {
  constructor(private name: string) {}
  toString() {
    return `Club: ${this.name}`;
  }
}
class User {
  constructor(private name: string, private club: Club) {}

  toString() {
    return `User: ${this.name}.\nClub info: ${this.club.toString()}`;
  }
}

// demo

const club = new Club("Football");
const user = new User("Alice", club);

console.log(user.toString());

The User class constructor depends on Club. In this case Club must be passed during initial phase of the User. Imagination, in your projects, you will have a tons of dependencies like this. What should you do?

IOC - Inversion of Control

Developer should keep their business logic clean by externalizing complicated and complex configuration, life cycle management and system interactions.

Complex tasks should be handled by IoC framework

import { Container, inject, injectable } from "inversify";
import "reflect-metadata";

// Define symbols for dependency injection
const TYPES = {
  Club: Symbol.for("Club"),
  User: Symbol.for("User"),
  ClubName: Symbol.for("ClubName"),
  UserName: Symbol.for("UserName"),
};

@injectable()
class IoCClub {
  constructor(@inject(TYPES.ClubName) private name: string) {}

  toString() {
    return `Club: ${this.name}`;
  }

  setName(name: string) {
    this.name = name;
  }
}

@injectable()
class IoCUser {
  constructor(
    @inject(TYPES.UserName) private name: string,
    @inject(TYPES.Club) public readonly club: IoCClub
  ) {}

  toString() {
    return `User: ${this.name}.\nClub info: ${this.club.toString()}`;
  }
}

const container: Container = new Container();

// Bind the dependencies
container.bind<string>(TYPES.ClubName).toConstantValue("Football");
container.bind<string>(TYPES.UserName).toConstantValue("Alice");
container.bind<IoCClub>(TYPES.Club).to(IoCClub);
container.bind<IoCUser>(TYPES.User).to(IoCUser);

// Get instances from container
const club = container.get<IoCClub>(TYPES.Club);
console.log(club.toString());

const user = container.get<IoCUser>(TYPES.User);
user.club.setName("Football Club");
console.log(user.toString());

But, the code looks more complex right?

When the complexity pays off:

The InversifyJS complexity becomes worthwhile in larger applications because:

  • Configuration centralization - All dependencies configured in one place
  • Lifecycle management - Singletons, transients, per-request instances
  • Testing - Easy to mock dependencies
  • Modularity - Different implementations for different environments

Conclustion

Simple manual DI

  • Small projects
  • Few dependencies
  • Prototype/demo code

Service Locator

  • Medium projects
  • Need some configuration management
  • Want testability without decorator complexity
// 1. Simple Service Locator Pattern
class ServiceContainer {
  private services = new Map<string, any>();

  register<T>(name: string, factory: () => T): void {
    this.services.set(name, factory);
  }

  get<T>(name: string): T {
    const factory = this.services.get(name);
    if (!factory) throw new Error(`Service ${name} not found`);
    return factory();
  }
}

// Usage - much simpler!
const container = new ServiceContainer();

container.register("clubName", () => "Football");
container.register("userName", () => "Alice");
container.register("club", () => new Club(container.get("clubName")));
container.register(
  "user",
  () => new User(container.get("userName"), container.get("club"))
);

const user = container.get<User>("user");
console.log(user.toString());

Factory Pattern

  • Complex object creation
  • Need environment-specific configurations
  • Clear separation of concerns
// 2. Factory Pattern with Configuration
interface AppConfig {
  clubName: string;
  userName: string;
}

class AppFactory {
  constructor(private config: AppConfig) {}

  createClub(): Club {
    return new Club(this.config.clubName);
  }

  createUser(): User {
    return new User(this.config.userName, this.createClub());
  }
}

// Usage
const factory = new AppFactory({ clubName: "Football", userName: "Alice" });
const user2 = factory.createUser();
console.log(user2.toString());

Full IoC Framework

  • Large enterprise applications
  • Complex dependency graphs
  • Need advanced features (decorators, modules, middleware)
  • Team development with strict patterns

TradeOffs

// Simple but rigid
const user = new User("Alice", new Club("Football"));

// Complex but flexible
const user = container.get<IoCUser>(TYPES.User);