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
↓ builds
↓ needs
↓ needs
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()
UserService
UserRepo
Db
✓ See it live
↓ needs
↓ needs
↓ needs
Resolve the graph — swap a binding, nothing else changes
UserService needs UserRepo, which needs Db. Resolving
builds bottom-up: Db → UserRepo → UserService.
📦 Container
resolve('UserService')
UserService
—
🗄️ SqlUserRepo
—
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
newon 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) vstransient(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.
Back to all topics →