skip to content

You implement a virtual proxy that creates an expensive object on first use. What correctness pitfalls must you handle?

level: middleimportance: should knowfreq 52%

answer

  1. Race on the null check → DCL / lazy holder
  2. Errors move from creation time to first use
  3. proxy != real: identity, casts, serialization
  4. equals() forces initialization
  5. Close an untouched proxy = no-op

basics

~20 s

Make the lazy creation thread-safe (two threads can both see "not created yet"), decide what happens if creation fails on first call, and remember the proxy is a different object from the real one — identity checks, equality, and type casts on the concrete class can break.

solid answer

~60 s

Four families of pitfalls. **Concurrency:** the naive `if (real == null) real = create()` is a race; two callers can both construct the subject, and with unsafe publication one may see a partially initialized object. Fix with double-checked locking over a volatile/atomic field, a lazy-holder idiom, or a language-provided lazy primitive — and decide whether creation may run twice harmlessly or must be exactly-once. **Deferred failure:** construction errors now surface at first *use*, far from the call site that "created" the proxy, often mid-transaction or after a response has begun streaming. Decide whether the failure is cached (subsequent calls fail fast) or retried. **Identity and type:** `proxy != real`; reference equality, identity maps, serialization, `instanceof`/casts to the concrete class, and reflection over declared fields all see the proxy. Forward `equals`/`hashCode`/`toString` where possible, but note that forwarding `equals` forces initialization, defeating laziness. **Resource lifetime:** if the subject holds a connection or file handle, the proxy must expose a way to close/release it — and closing an uninitialized proxy should not force creation just to close it.

code

java · 24 lines
java
class LazyIndex implements Index {
  private final String path;
  private volatile Index real;            // volatile = safe publication

  LazyIndex(String path) { this.path = path; }

  private Index real() {
    Index r = real;
    if (r == null) {
      synchronized (this) {               // double-checked locking
        r = real;
        if (r == null) real = r = new HeavyIndex(path);
      }
    }
    return r;
  }

  public List<Hit> search(String q) { return real().search(q); }

  public void close() {                   // no-op if never initialized
    Index r = real;
    if (r != null) r.close();
  }
}

go deeper

for a junior

Mention that the object is built on first use and that you must handle the case where two threads call at once, plus that errors now appear later.

for a middle

Give the concrete thread-safe idioms (volatile + double-checked locking, lazy holder) and note identity/equality surprises.

for a senior

Add failure-caching policy, transaction/stream-boundary hazards (lazy load after the session closed), resource cleanup on an untouched proxy, and when laziness is a net loss (N+1).

for a principal

Frame it as a latency/predictability trade: laziness moves cost and failure to the least predictable moment; argue for eager or batched loading at the boundary and reserve proxies for genuinely cold paths.

A **virtual proxy** stands in for an object that is expensive to create, and creates it only when a call genuinely needs it. This is the pattern behind ORM lazy associations, placeholder images, lazily built indexes, and deferred connection pools. The idea is trivial; the failure modes are not. ## 1. Thread safety of the lazy field The canonical bug: ``` render() { if (real == null) real = new Heavy(path) // RACE real.render() } ``` Two threads can both observe `null` and both construct. Worse, on memory models with reordering (JVM, C++), a thread can observe a **non-null but not fully constructed** object if the field isn't safely published. Safe options: - **Double-checked locking** with the field marked volatile/atomic: ``` if (real == null) { lock { if (real == null) real = new Heavy(path) } } ``` The inner re-check avoids constructing twice; the volatile/atomic marker gives correct publication. - **Lazy holder / initialize-on-demand** — put the construction in a nested type or static initializer the runtime initializes once, thread-safely, with no lock on the fast path. - **Compare-and-set** into an atomic reference: may construct more than once under contention but publishes exactly one winner — acceptable only if construction is side-effect free. - **Language primitives** — `Lazy`/`lazy(SYNCHRONIZED)`, `once_cell`, `sync.Once`, `functools.cached_property` (single-thread caveats). Decide explicitly: is *at-most-once construction* required (it opens a connection, registers a listener, mutates global state), or is *idempotent, possibly-twice* fine (pure computation)? ## 2. Deferred and repeated failure Lazy creation moves errors in time. Consequences to design for: - The stack trace at failure points at whoever first touched the object, not whoever configured it. Wrap the cause with context ("failed initializing report 42"). - **Failure caching:** if creation fails, does the next call retry (possibly hammering a broken dependency) or fail fast from a cached error? Both are legitimate; pick one and document it. A common middle ground is retry with backoff and a circuit breaker. - **Partial initialization:** if construction fails halfway, ensure the field stays null (or a sentinel) so you never publish a half-built subject. - **Bad interaction with transactions/streams:** an ORM proxy that lazy-loads after the session closed produces the classic "no session" error; a proxy that initializes after HTTP headers are flushed cannot turn its failure into a 500. This is the real reason lazy loading is contentious. ## 3. Identity, equality, and type The proxy is a *different object*: - `proxy == real` is false. Identity-keyed maps and reference-equality caches see two entries. - `equals`/`hashCode` must be forwarded to behave sensibly — but forwarding them **forces initialization**, so `map.contains(proxy)` silently triggers the expensive load. Some frameworks compare identifier fields stored on the proxy itself to avoid this. - `instanceof RealClass` / downcasts fail for interface-based proxies; reflection sees the proxy's fields, not the subject's. - Serialization of a proxy either serializes the placeholder (and the receiver has no way to resolve it) or forces initialization. Choose deliberately; many frameworks provide an "unproxy/unwrap" helper for exactly this. - Subclass-based proxies (generated by subclassing the real type) fix `instanceof` but need a non-final class and a callable constructor, and *their* inherited fields are unset — reading a field directly instead of through a method bypasses the proxy and returns null/zero. ## 4. Resource lifetime and cleanup If the real subject owns a handle: - `close()`/`dispose()` on an **uninitialized** proxy should be a no-op, not "create it so I can close it". - If the proxy is discarded without ever being used, nothing leaks — that's the win. If it *was* used, someone must close it; the proxy must expose that, and ownership must be clear. ## 5. Is laziness even worth it? Costs: extra indirection, harder debugging, temporal coupling, surprising errors. Benefits appear only when (a) creation is genuinely expensive, and (b) a meaningful share of instances are never used. If most instances are used immediately, eager creation is simpler and faster overall. Measure before you proxy — and prefer *batch/eager fetching* over N lazy proxies when the access pattern is "iterate everything" (the N+1 problem is a swarm of virtual proxies each doing one query). ## Testing checklist - Subject is **not** constructed when the proxy is constructed. - Subject is constructed exactly once across N concurrent first calls. - First-call construction failure surfaces with context; second call behaves per the documented retry policy. - Closing an untouched proxy does not construct anything. - Documented behavior for equality/identity is honored.

  • Why does forwarding equals() from a lazy proxy partly defeat the pattern?
    Because computing equality usually needs the subject's state, so the proxy must initialize it — meaning a mere `set.contains(x)` or map lookup triggers the expensive load you were trying to avoid. Frameworks dodge this by comparing an identifier the proxy already holds.
  • A page renders 200 lazily-proxied rows and issues 200 queries. What went wrong?
    The classic N+1: virtual proxies are right when most instances are never touched, but here the access pattern touches all of them. Replace per-item laziness with a batched/eager fetch (join or one IN-query) for that use case.
  • Is double-checked locking always the right fix?
    No. If construction is pure and cheap enough to risk duplicating, a plain compare-and-set is simpler; if the platform offers a lazy-holder idiom or a built-in lazy type, prefer that. DCL is only correct when the field has proper memory-visibility semantics (volatile/atomic).

saying these in an interview costs you the question

  • "The null check is fine, worst case we build it twice" — ignores unsafe publication of a half-built object
  • Assuming a plain non-volatile field makes double-checked locking correct
  • Forgetting that instanceof/casts to the concrete class fail through an interface-based proxy
  • Forcing initialization inside close()/dispose() just to release nothing
  • Treating lazy loading as free — it trades startup cost for unpredictable latency and late failures

context