skip to content

Why do many web frameworks build or validate the whole object graph at startup instead of resolving dependencies lazily on first use?

level: middleimportance: must knowfreq 58%

answer

  1. when does a broken promise become visible?
  2. boot failure versus first-request failure
  3. fail before traffic arrives
  4. validate edges without constructing
  5. laziness for genuinely optional components

basics

~20 s

Because it moves wiring failures from run time to boot time. A missing, ambiguous or cyclic binding then stops the process while no traffic depends on it, instead of breaking whichever request first walks that branch of the graph.

solid answer

~40 s

Nothing verifies a registration until something resolves it, so the question is only *when* a broken promise becomes visible. Resolving eagerly at startup turns a missing binding, an ambiguous binding or a cycle into a boot failure: reported once, with the resolution path, attributable to the deploy going out, and caught before the instance ever serves. Resolving lazily leaves the same defect to fire inside the first request that touches that branch — often a rarely-used endpoint, on an instance already reporting itself healthy. The cost of eager resolution is boot time, unused components built anyway, and constructor side effects that run unconditionally. Many containers offer a middle setting: **validate** every edge at startup without calling a constructor, which catches the same errors cheaply but cannot see inside factory registrations.

go deeper

for a junior

Remember the core trade: build everything up front and a wiring mistake stops the process, or build on demand and the same mistake breaks a request later. Know which failures are involved: missing, ambiguous, cyclic.

for a middle

Explain both directions honestly. Eager costs boot time, builds unused components and runs their side effects; lazy costs the guarantee. Mention the middle option of validating every visible edge without constructing anything.

for a senior

Argue it from operations: a failed boot is attributable to the release and stops a rollout, while a lazy failure surfaces on an instance already serving. Say where you would still allow laziness and why.

for a principal

Set the policy. Decide what the service must never start without, how boot time interacts with how often instances are created, and how much of the graph may hide behind factories where no validation can reach.

## Two moments a wiring error can surface A registration is a promise: *ask me for this key and I will build it*. Nothing verifies the promise until something resolves it. That leaves two candidate moments for a broken promise to become a visible failure: - **at startup**, while the process is still coming up and no traffic has reached it; - **at first use**, when a request arrives, a handler is resolved, and the walk down the graph reaches the missing or cyclic edge. The difference is not academic. The first is a process that never starts. The second is a process that starts, reports itself healthy, accepts traffic and then fails the subset of requests that touch the broken branch — often only the rarely-used endpoint nobody exercised before the release. ## What eager resolution buys Building the graph eagerly turns every wiring defect into a **boot failure**: - a missing binding, an ambiguous binding or a cycle is reported once, with the resolution path that reached it, instead of appearing inside an unrelated request's error; - the failure is attributable — the change that broke it is the change being deployed; - the deploy machinery can react, because a process that exits during startup is the clearest signal a rollout has to work with; - construction side effects that would otherwise happen during a request — opening pools, reading files, checking credentials — happen while nothing is waiting on them; - the first request pays no construction cost, which flattens the latency spike a cold instance shows just after it starts serving. ## What eager resolution costs - **Boot time** grows with the graph, and everything is built whether or not it is ever used. - **Unused branches are built anyway**, including expensive ones guarded by a feature that is off. - **Constructor side effects happen unconditionally**, so a component that connects to something unavailable in this environment can stop the process from starting at all. - **Cycles cannot be papered over**, which is usually a virtue but does remove an escape hatch. - Optional or environment-specific components need explicit handling rather than simply never being resolved. ## Validating without instantiating There is a third option between the two, and it is the one worth knowing about: many containers can **verify the graph** without building it. The check walks every registration, resolves each recipe's parameters to a binding, and reports missing, ambiguous or cyclic edges — but calls no constructor. | Strategy | When wiring errors surface | Boot cost | Side effects at boot | |---|---|---|---| | Lazy resolution | On the first request touching that branch | Lowest | None | | Graph validation only | At startup, for edges the container can see | Small | None | | Eager construction | At startup | Highest | All constructor side effects run | Validation does not see through factory registrations or anything resolved dynamically inside a component, so it narrows the window rather than closing it. Combining validation with eager construction of the expensive-but-essential parts, and laziness for the rest, is a common middle setting. ## When lazy is the right choice Laziness is not merely a weaker default. It is the right answer when: 1. the component is **genuinely optional** — present only when a feature is enabled or a setting is supplied, and never resolved otherwise; 2. construction is **expensive relative to the chance of use**, such as a client for a dependency only one rarely-called endpoint touches; 3. start time is itself a constraint, because instances are created and destroyed frequently and a slow boot is paid over and over; 4. a **cycle** has to be tolerated while it is being designed away, since deferring one side's construction is the usual stopgap. ## The operational reading The reason this question is asked in interviews is that the two strategies fail differently in production, not that one is cleaner. Eager resolution converts a class of defects into a failed deploy — loud, immediate, and attributable to the release. Lazy resolution converts the same defects into errors on a code path, discovered by whoever happens to call it, possibly days later, on an instance that has long since been declared healthy. A pragmatic position: validate the whole graph at startup wherever the container can, construct eagerly whatever the service cannot serve a request without, and keep laziness for components that are honestly optional. Then a wiring mistake is a deploy that does not go out, rather than an incident with a stack trace nobody expected.

  • What does graph validation catch that eager construction also catches, and what does it miss?
    It catches the structural errors — missing bindings, ambiguity and cycles — for every edge the container can read from a recipe's parameters, without paying construction cost. It misses anything resolved inside a factory body or fetched dynamically at run time, and it proves nothing about whether a constructor's side effects succeed, because none of them run.
  • When is lazy resolution clearly the better choice?
    When a component is genuinely optional — enabled by a setting and otherwise never resolved — or when construction is expensive relative to how rarely it is used. It also matters when instances start and stop often enough that boot time is paid repeatedly. Laziness is a deliberate choice for those cases, not a weaker default for the whole graph.
  • Why is a wiring failure at first request worse than the same failure at startup?
    Because the instance has already declared itself able to serve. The defect is attributed to whoever called the endpoint rather than to the release that introduced it, it may surface long after the deploy, and only the requests touching that branch fail — so overall error rates can stay low enough to hide it.

saying these in an interview costs you the question

  • Thinks a container verifies registrations at the moment they are registered
  • Believes eager resolution guarantees every dependency is actually reachable and working
  • Says lazy resolution is always faster overall rather than only at startup
  • Assumes graph validation can see dependencies resolved inside a factory function
  • Treats a missing binding at first request as a normal runtime error rather than a deploy defect