Skip to main content

IoC Containers & Advanced Dependency Management

Topic 7: IoC Containers & Advanced Dependency Management

1. Problem Statement

Case Study: Scaling the News Aggregator

Your news aggregator platform is growing:

  • You now fetch from dozens of sources, each with its own configuration.

  • You want to inject not just one, but many dependencies (e.g., logger, cache, analytics).

  • Manually wiring up dependencies everywhere is becoming error-prone and hard to maintain.

The problem:
How can you automate the creation and injection of dependencies, so your system is scalable, maintainable, and easy to test?

2. Learning Objectives

By the end of this tutorial, you will:

  • Understand what an IoC (Inversion of Control) container is.

  • Use a TypeScript IoC container (TypeDI) to manage dependencies.

  • Register and resolve dependencies automatically.

  • See how IoC containers simplify large-scale application development.

3. Concept Introduction with Analogy

Analogy: Hotel Concierge Service

Imagine a hotel guest (your class) needs various services: room cleaning, food delivery, taxi booking.

  • Instead of contacting each service directly, the guest calls the concierge (IoC container), who arranges everything behind the scenes.

  • The guest doesn’t care who provides the service, just that it’s delivered on request.

An IoC container is your concierge:
It manages all services (dependencies) and delivers them to your classes as needed.

What Is Inversion of Control (IoC)?

Inversion of Control is a principle where the flow of a program’s control is inverted:

  • Instead of your classes creating and managing their dependencies,

  • An external system (the IoC container) creates and supplies those dependencies.

Why use IoC?

  • This decouples your classes from specific implementations, making your code more flexible, testable, and maintainable.

What is an IoC Container?

  • An IoC Container is a framework that manages the creation, configuration, and injection of dependencies automatically.

  • Instead of manually creating dependencies, you register them with the container and request them when needed.

5. Step-by-Step Data Modeling & Code Walkthrough

Step 1: Install TypeDI**

npm install typedi reflect-metadata
  • In your tsconfig.json, enable decorators:
 "experimentalDecorators":  true, 
"emitDecoratorMetadata": true
  • At the very top of your entry file (e.g., index.ts):
import "reflect-metadata";

Step 2: Define Interfaces and Implementations

// src/NewsSource.ts
export interface NewsSource {
fetchArticles(): Promise<string[]>;
}
// src/RSSFeedSource.ts
import { Service } from "typedi";
import { NewsSource } from "./NewsSource";

@Service()
export class RSSFeedSource implements NewsSource {
async fetchArticles(): Promise<string[]> {
return ["RSS: Article 1", "RSS: Article 2"];
}
}

// src/APISource.ts
import { Service } from "typedi";
import { NewsSource } from "./NewsSource";

@Service()
export class APISource implements NewsSource {
async fetchArticles(): Promise<string[]> {
return ["API: Article A", "API: Article B"];
}
}

  • @Service() registers the class with TypeDI’s container.

Step 3: Inject Dependencies Automatically

// src/NewsAggregator.ts
import { Service, Inject } from "typedi";
import { NewsSource } from "./NewsSource";

@Service()
export class NewsAggregator {
constructor(
@Inject(() => RSSFeedSource) private source: NewsSource
) {}

async getLatestArticles() {
const articles = await this.source.fetchArticles();
articles.forEach(article => console.log(article));
}
}
  • @Inject(() => RSSFeedSource) tells TypeDI which implementation to inject.

Step 4: Resolve and Use

// src/index.ts
import "reflect-metadata";
import { Container } from "typedi";
import { NewsAggregator } from "./NewsAggregator";

const aggregator = Container.get(NewsAggregator);
aggregator.getLatestArticles(); // Uses RSSFeedSource by default
  • Container.get() creates the class and injects all dependencies.

Step 5: Swapping Implementations

import { Container } from "typedi";
import { NewsAggregator } from "./NewsAggregator";
import { APISource } from "./APISource";
import { NewsSource } from "./NewsSource";

// Override the NewsSource dependency
Container.set(NewsSource, new APISource());

const aggregator2 = Container.get(NewsAggregator);
aggregator2.getLatestArticles(); // Now uses APISource

  • No changes to NewsAggregator code needed!

Step 6: Swapping Implementations

Suppose you want to use APISource instead of RSSFeedSource:

container.rebind<NewsSource>(TYPES.NewsSource).to(APISource);

const aggregator2 = container.resolve(NewsAggregator);
aggregator2.getLatestArticles(); // Now uses APISource
  • No changes to NewsAggregator code needed!

Step 7: How Does TypeDI Know What to Inject?

  • Decorators:

    • @Service() marks a class as injectable.

    • @Inject() specifies which dependency to inject.

  • Metadata:

    • TypeDI uses TypeScript’s reflect-metadata to read type information and decorator hints.

6. Challenge

  1. Register a new APISource with the container.

  2. Swap the implementation from RSSFeedSource to APISource without changing the NewsAggregator code.

  3. Write a test that injects a mock source to verify the aggregator’s behavior.

7. Quick Recap & Key Takeaways

  • IoC Containers automate dependency management.

  • Register, configure, and swap dependencies centrally.

  • Classes remain clean, focused, and testable.

8. (Optional) Programmer’s Workflow Checklist

  • Define interfaces for dependencies.

  • Register implementations with the container.

  • Use decorators (@injectable, @inject) for automatic injection.

  • Swap implementations for testing or scaling.