skip to content

How would you set a default component lifetime across a large service, and what should push a component off that default?

level: principalimportance: should knowfreq 38%

answer

  1. a default nobody has to think about
  2. state and owned resources decide
  3. validate the graph at startup
  4. keep the scoped set short and published

basics

~20 s

Default to process-wide components holding no caller state, and move one to the request scope only when it carries a single request's state or owns a resource released with that request. Enforce it with lifetime validation at startup.

solid answer

~50 s

A default that everyone can apply without thinking beats a per-component optimum nobody applies consistently. My default is **process-wide and stateless**: collaborators hold configuration, pooled resources and other collaborators, never caller data. A component earns the **request scope** only by holding one request's state or owning a resource whose release must be tied to that request; transient is reserved for cheap stateful helpers. Two blanket policies fail in opposite directions: everything process-wide invites hidden shared state, while everything per-request rebuilds a deep object graph on every call and pays for it in allocation and tail latency at peak. The guardrails matter more than the taxonomy — validate the graph at startup so a longer-lived component holding a shorter-lived one fails the boot, keep the request-scoped set short enough to list on one page, and treat a component's lifetime as part of its published contract.

go deeper

for a junior

You will rarely set this policy, but follow it: use the codebase default unless the component holds one request's state, and ask before introducing a new request-scoped component.

for a middle

Be able to justify a single registration against the rule — what state does it hold, what does it own — rather than copying the lifetime of the file next to it.

for a senior

Show that you weigh allocation against thread-safety risk with numbers from the tail, and that you keep the scoped set small on purpose rather than by accident.

for a principal

Own the policy: one written default, startup validation in the build, a published exception list, and a clear position that an unstated default is the real defect in a large codebase.

## Why a default matters more than the optimum Lifetime decisions are made hundreds of times across a large codebase, mostly by people who are thinking about something else. A rule that is right 90% of the time and costs no thought produces better outcomes than case-by-case optimisation that is applied unevenly and re-litigated in review. So the question to answer first is not "what is best for this component" but "what should a team assume when nobody thought about it". ## The default that works **Process-wide, holding no caller state.** Most components in a service are collaborators: they hold parsed settings, pooled resources and references to other collaborators. Built once, they cost nothing per request and can be reasoned about as constants. The obligation that comes with the default is real and must be stated alongside it: a shared component's mutable fields are shared by every concurrent request. The default is therefore not "singleton" on its own — it is **"singleton and stateless"**, and the second half is the part reviewers enforce. ## What legitimately moves off the default 1. **It carries one request's state.** Identity, tenant, locale, correlation values, an accumulating unit of work — anything whose correctness depends on which request is asking. 2. **It owns a resource that must be released with the request.** A borrowed connection, a buffer, an open transaction. The request scope exists so release is deterministic rather than eventual. 3. **It is cheap, stateful and genuinely per-use.** A builder or accumulator that must not be shared between two callers in the same request is the narrow case for transient. Anything else — expensive construction, wide fan-in, heavy concurrent use — is *not* a reason to shorten a lifetime. Expensive construction argues for sharing. Concurrent use argues for statelessness, not for a new instance per request. ## The cost model | Policy | What it buys | What it costs | |---|---|---| | Everything process-wide | Minimal allocation, simple graph, predictable latency | Hidden shared state; a single stateful field becomes a data-correctness bug under load | | Everything per-request | No thread-safety reasoning; deterministic release | A deep graph rebuilt per request; allocation and tail latency at peak; resources re-acquired constantly | | Stateless default, small scoped set | Both benefits where they matter | Requires a stated rule and a mechanical check to hold the line | The third row is only stable if the "small scoped set" really stays small. Each request-scoped component adds edges that longer-lived components may not hold directly, so the review burden grows with the size of that set, not with the size of the codebase. ## Guardrails, in order of value - **Validate the graph at startup.** Reject any edge from a longer lifetime to a shorter one, with factory, accessor or proxy injection as the sanctioned exception. This converts the most common lifetime bug from a silent production data defect into a boot failure that no deploy can skip. - **Construct eagerly at boot.** Lazy construction defers discovery of the same defects to whichever request happens to trigger them, which means the first evidence arrives in production. - **Publish the scoped set.** A short list of request-scoped components, with a sentence each on why, is a document people actually read. A long list is evidence the default has eroded. - **Make lifetime part of the contract.** For a shared library component, its lifetime constrains every holder, so changing it later is a breaking change even though no signature moved. State it where the component is documented. ## What to measure - Allocation and latency at the tail, not the mean: the cost of a deep per-request graph lands on the slowest percentiles first. - Pool in-use counters returning to baseline between traffic bursts, which is the cheapest live evidence that scopes really close. - The count of request-scoped registrations over time. A number that only grows is the leading indicator that the default is being worked around rather than applied. ## The organisational angle The real failure mode in a large codebase is not a wrong lifetime; it is **an unstated default**, under which every team invents its own and the graph accumulates edges nobody validated. The strongest position to argue is therefore: one written default, one mechanical check in the build, one short list of exceptions, and a bias toward passing request data as arguments rather than scoping more components to carry it.

  • Why is a component's lifetime part of its contract rather than an internal detail?
    Because it constrains every holder. A component that may be process-wide can be held directly by anything; one that is request-scoped may only be reached through a late-resolving seam. Changing a published component from one to the other silently invalidates existing wiring without changing a single signature, so it is a breaking change.
  • What is the cheapest guardrail against lifetime mistakes across many teams?
    Startup validation of the graph. It is configuration rather than process, it runs on every boot in every environment, and it rejects the longer-holds-shorter edge that produces the worst class of lifetime bug. Review checklists and integration tests both miss it, because one request at a time is exactly the case that behaves correctly.
  • When is 'make everything request-scoped' a defensible choice?
    In a low-traffic service where simplicity dominates: no thread-safety reasoning, deterministic release everywhere, and allocation that never matters. It stops being defensible when request rates make graph construction visible in the tail, or when components own resources whose acquisition cost per request exceeds the work of the request itself.

saying these in an interview costs you the question

  • Argues everything should be request-scoped because allocation is effectively free
  • Picks lifetimes case by case with no written default for the codebase
  • Treats a shared component's lifetime as an internal detail holders need not know
  • Cites expensive construction as a reason to shorten a component's lifetime
  • Relies on review checklists alone instead of validating the graph at startup