skip to content

When implementing a Singleton, what are the trade-offs between creating the instance eagerly at startup versus lazily on first use?

level: middleimportance: should knowfreq 55%

answer

  1. eager = free thread-safety + fail fast
  2. lazy = pay only if used, needs run-once guard
  3. first caller eats the latency spike
  4. failed lazy init poisons later accesses
  5. DI container makes eager/lazy a config flag

basics

~20 s

Eager creation happens at startup: simple, thread-safe for free, and failures show up immediately — but you pay the cost even if the object is never used. Lazy creation defers the cost to first use, at the price of needing thread-safe initialization.

solid answer

~50 s

Eager initialization builds the instance when the class or module loads. It needs no synchronization (the runtime's initialization is once-only and safely published), it fails fast — a bad config or unreachable resource blows up at startup rather than mid-request — and it removes first-call latency. The costs: startup time and resources are spent even for code paths never taken, initialization ordering between eagerly created objects becomes significant, and the object cannot depend on configuration that arrives after load. Lazy initialization defers construction to the first accessor call, so unused subsystems cost nothing and the object can read state that only exists later. Its costs are real: you must make the check-and-create thread-safe, the first caller absorbs the latency (bad if that's a request thread), errors surface late and in an unrelated place, and construction ordering becomes emergent and hard to reason about. Default to eager for small/critical objects; go lazy for expensive or optional ones.

go deeper

for a junior

Say eager builds it at startup and lazy builds it on first use; eager wastes work if unused, lazy needs care with threads.

for a middle

Add fail-fast versus fail-late, the first-call latency spike, and that eager gets thread safety from the runtime's once-only class initialization.

for a senior

Discuss error semantics of failed lazy initialization, nondeterministic construction order, reentrancy deadlock, and the eager-wrapper/lazy-internals middle ground.

for a principal

Frame it operationally: eager singletons plus a readiness probe make bad deploys self-rejecting; make eager/lazy a container-level, per-environment policy rather than a decision hard-coded in each class.

## The choice A Singleton's instance has to be built at *some* moment. Two options: - **Eager**: built when the class/module is initialised (usually process startup or first touch of the class). - **Lazy**: built the first time someone calls the accessor. ## Arguments for eager 1. **Thread safety for free.** On runtimes with a class-initialization phase, that phase runs exactly once under an internal lock with a memory barrier. No locks, no double-checked locking, no publication bug. 2. **Fail fast.** If construction validates config, opens a file, or resolves a hostname, an eager singleton turns a latent misconfiguration into a startup crash. That is enormously valuable operationally: a deploy that fails at boot is rolled back automatically; one that fails on the first request at 3 a.m. is an incident. 3. **No first-call latency spike.** With lazy init the unlucky first request pays the whole construction cost — connection handshakes, cache warm-up, JIT of the init path. That shows up as tail-latency noise and as flaky timeouts right after a deploy. 4. **Simpler to reason about.** The object exists or the process doesn't. ## Arguments for lazy 1. **You don't pay for what you don't use.** A CLI with 40 subcommands should not open a database pool for `--help`. Startup time matters for serverless cold starts, CLIs, tests, and desktop launch. 2. **Construction can depend on state that isn't ready at load time.** Config resolved from a remote source, a user-selected profile, a plugin registered after boot. 3. **Optional/conditional subsystems.** Feature-flagged components that most deployments never enable. 4. **Breaks initialization-order cycles** — sometimes A can only be built after B, and deferring both to first use side-steps a static ordering puzzle (though this is treating a symptom). ## The hidden costs of lazy - **Thread safety becomes your problem**: the check-then-act race and the unsafe-publication problem (see the thread-safety discussion). Use a holder idiom or the platform's run-once primitive rather than hand-rolled locking. - **Error timing and error shape**: a failure inside lazy construction surfaces deep in a call stack, in a request, possibly wrapped by the runtime into an unrelated initialization error. Worse, some runtimes mark a type whose initialization threw as permanently unusable, so every *subsequent* access throws a different and far less informative error than the first. - **Nondeterministic ordering**: which singleton is built first now depends on traffic, so behaviour differs between a load test and production, and between two runs of the same test suite. - **Reentrancy deadlock**: if construction transitively calls the accessor again, a lock-based lazy implementation deadlocks. - **Observability**: startup logs no longer tell you what the process actually has. ## A useful decision rule | Situation | Prefer | |---|---| | Small object, always used, validates config | **Eager** — fail fast, zero complexity | | Expensive resource used by most requests | **Eager**, plus a readiness probe that waits for it | | Expensive resource used by a rare path | **Lazy** with a run-once primitive | | CLI / serverless where startup latency is the product | **Lazy** for everything not on the hot path | | Depends on config that arrives after boot | **Lazy** (or restructure so it doesn't) | ## The framework answer Dependency-injection containers make this a *declaration* rather than an implementation detail: a bean/service can be registered as eager or lazy, and the container handles once-only creation, ordering, and safe publication. Frameworks typically default to eager singletons precisely for fail-fast reasons, with an opt-in lazy flag. That is another argument for letting the container own instance lifetime instead of encoding it in a static accessor — you can flip the strategy per environment (lazy in dev for fast reloads, eager in prod for fail-fast) without touching the class. ## Middle ground - **Eager with lazy internals**: construct the wrapper eagerly (so it's registered, observable, and config-validated) but defer the expensive part (open the socket, warm the cache) to first use or a background warm-up. - **Background warm-up**: build lazily but kick off construction on a startup thread so the first real caller usually finds it ready. - **Explicit initialization phase**: an `init()` called from a composition root, giving you an ordered, observable, testable startup with none of the static-initialization magic.

  • Which do dependency-injection frameworks usually default to, and why?
    Eager. Creating all singletons at startup surfaces missing configuration, unresolvable dependencies, and unreachable resources while the process is still starting, so a bad deploy fails its health check instead of failing a user request. Lazy is normally an opt-in flag for expensive or rarely used components.
  • Your lazily initialised singleton throws during construction. What does the next caller see?
    It depends on the mechanism. With class-initialization-based laziness the type is marked erroneous and later accesses throw a distinct, far less informative error — the original cause appears only in the very first failure. With a lock-and-field implementation the field stays unset and the next caller retries, which helps for transient faults but can loop forever on permanent ones.
  • How do you get lazy's startup savings without the first-caller latency spike?
    Trigger construction from a background warm-up task at startup while keeping the accessor lazy and thread-safe. Real traffic then usually finds the instance ready, and if it doesn't, it simply blocks on the same once-only guard rather than duplicating work.

Eager is turning on all the lights when you enter the building: instant cost, but you find the burnt-out bulb immediately. Lazy is motion sensors: cheaper when rooms are empty, but the first person into a room discovers the dead bulb — in the dark, at the worst moment.

saying these in an interview costs you the question

  • "Lazy is always better because it's faster" — it moves cost onto the first request and delays error discovery.
  • "Eager isn't thread-safe" — runtime class initialization is once-only and safely published; eager is the *simplest* thread-safe option.
  • Choosing lazy to break an initialization cycle without acknowledging the cycle is the real design problem.
  • Ignoring that a failed lazy initialization can poison the type so later errors no longer name the real cause.
  • Treating the choice as permanent when a DI container makes it a per-environment configuration flag.

context