When is a dependency-injection container worth its cost over hand-written wiring, and how do you keep a large container-wired graph understandable?
answer
- both build the same graph
- size and churn against verification
- two composition roots is the worst case
- declared dependencies, never fetched ones
- make the graph check automated
basics
~20 sA container pays off when the graph is large and churns, or the framework already resolves handlers through one; a small stable graph rarely repays it. Readability rests on one composition root, injected not fetched dependencies, and automated graph checks.
solid answer
~40 sBoth approaches produce the same graph, so the decision is about verification, effort and readability. Hand-written wiring is checked where it is written and reads top to bottom, which suits a small, stable graph and a start-up-sensitive service. A container makes the twentieth service cost what the second did and handles per-environment differences well, which suits a large, churning graph — and if the framework already resolves handlers through a container, wiring the rest by hand just creates a second composition root. The cost is errors reported at resolution rather than where written, plus a graph you read by searching registrations. Keeping it understandable is about invariants: one composition root, dependencies declared not fetched, the graph acyclic and shallow, and an automated check that resolves or validates every registration.
go deeper
Know that wiring can simply be written by hand in one startup function, and that a container is automation of that same job rather than a different capability.
Compare the two on concrete axes: how much repetition each carries, when a wiring error is detected, and how a reader finds which implementation is behind a parameter.
Bring operational judgment — start-up cost for short-lived instances, per-environment differences, and the automated check that keeps a growing graph honest before a deploy.
Own the invariants rather than the mechanism: one composition root, declared not fetched dependencies, an acyclic graph, and automated verification. Be able to say what would make you move a service off its current choice.
## The decision is about where construction knowledge lives Every service has a **composition root**: the one place that knows how the whole object graph is assembled. The only question is whether that place is code a person wrote and the compiler checked, or a set of registrations a container interprets while starting. Both produce the same graph. The tradeoff is about verification, effort and readability — not about capability, and not about how loosely coupled the code is, which is decided by what constructors name. ## The axes that actually decide it - **Size and churn of the graph.** Hand-wiring is linear work that repeats; a container makes adding the twentieth service cost the same as the second. Below roughly a few dozen components with stable edges, the repetition is cheap and the indirection is not. - **When errors are caught.** Wiring written by hand is usually checked where it is written. Wiring interpreted at run time is checked when it resolves — later, and with a message about keys rather than about code. Containers that generate wiring ahead of time narrow that gap. - **How much construction is not the framework's choice anyway.** Handlers are constructed by the framework on demand; if it already resolves them through a container, hand-wiring only the rest leaves you with two composition roots, which is worse than either alone. - **Traceability.** "Which implementation is behind this parameter, and who decided?" is answered by reading a function in one case, and by searching registrations and inferring the match in the other. - **Start-up cost and cold starts.** Reflective graph construction is work done every time a process starts. Where instances start and stop constantly, that cost is paid repeatedly. - **Team familiarity.** Wiring that most of the team cannot predict is a tax on every change, regardless of which mechanism produced it. ## Where each one earns its keep | Situation | Leans toward | |---|---| | Small, stable graph; few services; start-up time is critical | Hand-written composition root | | Large graph, frequent additions, many shared collaborators | Container | | The framework already resolves handlers through a container | Container, used consistently | | Wiring must be verified before it runs, with no start-up check | Hand-written, or an ahead-of-time container | | Wiring differs per environment in more than a couple of places | Container, with the differences isolated | ## Keeping a large graph understandable Scale is what makes a container hard to read, so the practices worth enforcing all restore locality: 1. **One composition root.** Registration happens in wiring code, grouped by feature area, and nowhere else. A graph assembled in several unrelated places cannot be read at all. 2. **No lookups outside it.** Components take what they need as constructor parameters; nothing reaches into the container mid-request. This keeps every dependency visible in a signature and keeps the graph a graph rather than an ambient registry. 3. **Keep the graph shallow and acyclic.** Long chains make a single missing binding produce a deep, unreadable resolution path; rings make components inseparable. 4. **Prefer registrations a tool can read.** The more edges hide inside factory bodies, the less any validation can prove, and the more of the graph exists only in someone's head. 5. **Verify the graph automatically.** A fast check that resolves or validates every registration turns the container's own knowledge into a test, which is the cheapest insurance available. 6. **Name environment differences, not implementations.** Isolating the handful of registrations that differ per environment keeps the rest of the graph identical everywhere it runs. ## The failure mode to design against The bad end state is not "too many registrations". It is a graph nobody can trace: dependencies fetched mid-request instead of injected, edges buried in factories, registrations scattered across the codebase, and a resolution error whose path crosses ten components nobody owns. That state is reached gradually, and each individual step looks reasonable. The judgment a lead is expected to bring is therefore less "container or not" than **what must stay true about the graph as it grows**: construction lives in one place, dependencies are declared rather than fetched, the graph is acyclic, and something automated fails when it is not. A team that holds those four invariants can use either mechanism safely; a team that holds none will find a container amplifies the confusion faster than hand-wiring would.
- Why is mixing a container with hand-written wiring in the same service usually a poor outcome?It creates two composition roots. A reader tracing one object's origin must know which mechanism built it, shared instances risk being constructed twice under different rules, and no single check covers the whole graph. Picking one mechanism for everything the framework does not force, and isolating any exception deliberately, is worth more than the local convenience.
- What should an automated check over the container graph actually assert?That every registration resolves: each key has exactly one recipe where one is expected, every recipe's parameters have bindings, and there are no cycles. Running it in the normal fast test suite turns startup-only knowledge into a check that fails before the change is pushed, which is where wiring errors are cheapest to fix.
- How does start-up cost change the calculation for short-lived instances?Reflective graph construction is work repeated on every process start. Where instances are created and destroyed constantly, that cost is paid continuously rather than once, so it favours a smaller eagerly-built core, wiring resolved ahead of time where the container supports it, or a hand-written root for the hot path.
saying these in an interview costs you the question
- Argues a container is always correct because hand-wiring does not scale
- Measures the decision by lines of wiring code rather than by verification and traceability
- Accepts components fetching dependencies mid-request as a normal way to use a container
- Runs a container and a hand-written root side by side without isolating the exception
- Assumes a large registration set is by itself a sign the graph is unhealthy