You implement a virtual proxy that creates an expensive object on first use. What correctness pitfalls must you handle?
answer
- Race on the null check → DCL / lazy holder
- Errors move from creation time to first use
- proxy != real: identity, casts, serialization
- equals() forces initialization
- Close an untouched proxy = no-op
basics
~20 sMake 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 sFour 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 linesclass 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
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.
Give the concrete thread-safe idioms (volatile + double-checked locking, lazy holder) and note identity/equality surprises.
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).
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