awesome-everything RU
↑ Back to the climb

Backend Architecture

DI containers in production: resolution graphs, circular deps, and when not to

Crux A container is a graph resolver: it topologically sorts dependencies and instantiates them once. The sharp edges — circular deps it cannot order, eager startup that surfaces config bugs at boot, and a real cost meaning not every app needs one.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 17 min

An app boots fine in development and crashes on the production server with Nest can't resolve dependencies of the UserService (?, AuthService). Nothing about UserService changed. What changed is that AuthService now imports UserService, and UserService already imported AuthService — a cycle. In dev the modules happened to load in an order that papered over it; in prod they did not. The container is not buggy. It is telling you it cannot put a circle in a line.

A container is a graph resolver

Strip away the decorators and a DI container does one thing: it reads the dependency declarations, builds a directed graph, and instantiates in dependency order — leaves first, then the things that depend on them, up to the root. This is a topological sort. To create OrderService(db, payments), it must first create db and payments; to create those it resolves their dependencies, recursively. For singletons it does this once and caches the instance, so the second consumer of db gets the same object. The container is the automated composition root from earlier in the unit — the one place that knows concretions, now mechanized.

Why circular dependencies break it

A topological sort only exists for a directed acyclic graph. If A depends on B and B depends on A, there is no “leaves first” order: to build A you need B, but to build B you need A. The container detects the cycle and refuses, which is the Hook’s error. Frameworks offer an escape hatch — NestJS’s forwardRef(() => Other) — that breaks the deadlock by deferring one side’s resolution until after construction. But the escape hatch is a smell, not a solution: a cycle almost always means two classes that should be one, or a missing third class that both should depend on. Reaching for forwardRef to silence the error preserves the design flaw.

Why this works

Why does a cycle sometimes appear only in production? Resolution order can depend on module load order, and module load order can differ between a dev server doing hot-reload and a cold production boot, or between two bundlers. With a true cycle, some orders happen to resolve (the first class is partially constructed when the second asks for it and gets the half-built reference) while others throw. That makes the cycle a latent bug that surfaces nondeterministically — green locally, red on deploy. The real fix is to remove the cycle: extract the shared logic both classes need into a third class they both depend on, turning the circle into a tree. forwardRef only hides that the graph is not acyclic.

Eager vs lazy: when failure surfaces

Containers differ on when they instantiate. Eager instantiation builds the whole singleton graph at startup, before serving traffic — NestJS does this by default. Lazy instantiation creates each provider on first use. The tradeoff is when bad configuration bites: eager startup means a missing env var or unresolvable dependency crashes the boot — loud, immediate, before any user is affected. Lazy means the same misconfiguration hides until the first request hits that code path, possibly hours later, possibly only on one endpoint. For server applications the senior preference is overwhelmingly eager: pay a slightly slower boot to convert a 3 a.m. runtime page into a deploy-time failure your CI catches.

The hidden costs

A container is not free. It adds startup time (building the graph), a learning and debugging tax (stack traces run through framework resolution code, and “cannot resolve” errors are their own skill to read), and a temptation toward hidden global state — the container becomes a place to stash singletons that are really just globals with extra steps. For a small service, a CLI, or a serverless function with a tiny graph, hand-wiring in a composition root — plain new calls in one file at the entry point — is often clearer and faster than a container. The container earns its cost when the graph is large, lifetimes are mixed, and the wiring would otherwise be a sprawling manual mess. “Use a DI container” is a scale decision, not a default.

ConcernEager (startup)Lazy (first use)
Missing dependencyCrashes at boot, before trafficCrashes mid-request, later
Boot timeSlower (builds whole graph)Faster
Best forLong-running serversCLIs, rarely-hit paths
Failure visibilityLoud, deploy-timeQuiet, runtime
Quiz

Why can a DI container not resolve a circular dependency between two services?

Quiz

Why do server applications usually prefer eager instantiation of the dependency graph at startup?

Quiz

When is hand-wiring in a composition root often preferable to adopting a DI container?

Recall before you leave
  1. 01
    What does a DI container actually do under the hood, and how does it handle singletons?
  2. 02
    Why do circular dependencies break a container, why might one appear only in production, and what is the real fix?
  3. 03
    Compare eager and lazy instantiation, and explain when a container is worth its cost versus hand-wiring.
Recap

Beneath the decorators, a DI container is a graph resolver: it turns dependency declarations into a directed graph and instantiates them in topological order, building singletons once and caching them — the composition root, mechanized. That framing explains its sharpest edge: a circular dependency has no valid construction order, so the container refuses, and because resolution can follow module load order the same cycle can pass in dev and crash in prod. forwardRef defers one side to break the deadlock but only masks a graph that should have been made acyclic by extracting a shared third class. Containers also choose when to build: eager startup instantiation turns misconfiguration into a loud deploy-time crash, which long-running servers should prefer over a quiet first-request failure. And the machine is not free — startup cost, a debugging tax, and the pull toward hidden global state mean a small graph is often better hand-wired in a plain composition root. With middleware and DI both understood — the request axis and the wiring axis, their mechanics, scopes, seams, and production edges — the track can move on to how blocking and async work shape throughput under load.

Connected lessons
appears again in185
Continue the climb ↑Middleware and DI: multiple-choice review
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.