design·lab

Real-world combination

Dependency Injection Container

A composition-root container builds and wires your whole object graph — so nothing new's up its own dependencies deep inside a class again.

✗ The problem

Objects build their own dependencies

A class reaches out and constructs exactly the concrete thing it needs, buried deep inside its own constructor.

class UserService {
  constructor() {
    // hard-wired, buried deep inside:
    this.repo = new SqlUserRepo(new Db());
  }
}
Tight coupling to SqlUserRepo — can't swap it for another implementation, can't unit-test UserService without a real Db, and this same wiring is scattered across every class that new's up its own deps.
✓ The combination

One container builds the whole graph

At the composition root you register services (with a lifetime — transient or singleton), then resolve by name. The container constructs dependencies in order and injects them through the constructor.

container.register('Db',       () => new Db(),                                 'singleton');
container.register('UserRepo', c  => new SqlUserRepo(c.resolve('Db')), 'transient');
container.register('UserService', c => new UserService(c.resolve('UserRepo')));

const service = container.resolve('UserService');
Container
register() / resolve()
↓ builds
UserService
↓ needs
UserRepo
↓ needs
Db
✓ See it live

Resolve the graph — swap a binding, nothing else changes

UserService needs UserRepo, which needs Db. Resolving builds bottom-up: DbUserRepoUserService.

📦 Container
resolve('UserService')
↓ needs
UserService
↓ needs
🗄️ SqlUserRepo
↓ needs
Db
not resolved yet

Tip: swap the binding to InMemoryUserRepo (what a test would use), then resolve again — UserService's code never changes.

✓ Takeaway

Wiring in one place, not scattered everywhere

  • Composition root: all construction lives in one place — nowhere else calls new on a dependency directly.
  • Swap freely: change a binding to inject a fake/mock in tests, or a new implementation in prod — consumers never change.
  • Lifetimes: singleton (one shared instance) vs transient (fresh every resolve) — the container manages it, not the object.
  • Caution: don't over-use it. A container that hides the whole graph behind magic strings makes code harder to trace — keep registration explicit and close to the composition root.
🎯 Combines: Factory (construct) + Singleton (lifetimes) + a registry — the machinery that makes Dependency Inversion practical.