Lazy Initialization — Optimization Drills¶
Category: Object & State Patterns — defer creating an expensive value until first use, then cache it.
10 inefficient implementations + benchmarks + optimizations.
Apple M2 Pro (ARM), single thread, post-warmup. Illustrative — measure your own.
Optimization 1: Replace Eager Construction with Lazy¶
Slow¶
Optimized¶
class Report {
private byte[] thumb;
byte[] thumb() { if (thumb == null) thumb = render(); return thumb; }
}
Effect: construction drops from 200 ms to ~0; the 200 ms is paid only by reports someone views. If 90% are never viewed, you save 90% of the rendering work.
Optimization 2: Replace synchronized Getter with the Holder Idiom¶
Slow¶
public static synchronized Registry getInstance() { // locks EVERY call
if (instance == null) instance = new Registry();
return instance;
}
Locking on every access, forever, for a one-time init.
Optimized¶
private static final class Holder { static final Registry I = new Registry(); }
public static Registry getInstance() { return Holder.I; }
Benchmark¶
The holder idiom is lazy, thread-safe, and the read compiles to a plain static load.
Optimization 3: volatile DCL Instead of Locking Per Access¶
Slow¶
public synchronized Heavy heavy() { // lock on every read
if (heavy == null) heavy = new Heavy();
return heavy;
}
Optimized¶
private volatile Heavy heavy;
public Heavy heavy() {
Heavy local = heavy;
if (local == null) {
synchronized (this) {
local = heavy;
if (local == null) heavy = local = new Heavy();
}
}
return local;
}
Benchmark¶
Read the volatile into a local once — avoids a second volatile load on the fast path.
Optimization 4: sync.Once vs Mutex-Every-Call (Go)¶
Slow¶
func (s *Service) Conn() *Conn {
s.mu.Lock(); defer s.mu.Unlock() // lock every call
if s.conn == nil { s.conn = dial() }
return s.conn
}
Optimized¶
func (s *Service) Conn() *Conn {
s.once.Do(func() { s.conn = dial() }) // atomic-load fast path after init
return s.conn
}
Benchmark¶
sync.Once reaches a lock-free fast path (single atomic load of its done flag).
Optimization 5: cached_property vs @property + Flag (Python)¶
Slow¶
@property
def total(self): # method call + branch on EVERY access
if self._total is None:
self._total = sum(self.rows)
return self._total
Optimized¶
@functools.cached_property
def total(self):
return sum(self.rows) # descriptor shadowed after 1st access
Benchmark¶
After the first hit, cached_property is a dict lookup; @property pays a method call forever.
Optimization 6: Warm the Lazy Value at Startup to Kill the Spike¶
Problem¶
A lazy connection pool makes the first request after deploy eat a 600 ms dial — blowing p99.
Optimized — warm in the background¶
func main() {
svc := NewService(addr)
go svc.Conn() // warm asynchronously; first real request finds it ready
serve(svc)
}
// Java: touch critical lazies post-boot on a background thread
Executors.newSingleThreadExecutor().submit(() -> service.heavy());
Effect: you keep laziness's "don't build the unused" while moving the spike off the request path. Lazy total work, eager latency profile.
Optimization 7: Don't Lazy-Init Cheap Values¶
Slow (over-engineered)¶
private volatile Integer hash;
public int hashCode() {
Integer h = hash;
if (h == null) { h = compute(); hash = h; } // guard + volatile cost > the work
return h;
}
For a cheap compute(), the volatile read + branch + boxing costs more than recomputing.
Optimized — just compute, or eager-cache a primitive¶
private final int hash = compute(); // eager, if always used
// or, for the String-style trick, a non-volatile int with the 0-means-uncomputed idiom
Lesson¶
Lazy init has overhead (branch, barrier, mutable state). Below a cost threshold, eager or plain recompute wins.
Optimization 8: Fix N+1 by Eager-Fetching the Known Path¶
Slow¶
Optimized¶
orders = Order.objects.prefetch_related("items")
total = sum(o.items.count() for o in orders) # 2 queries
Benchmark (500 orders)¶
Lazy is the right default; eager-fetch once you know you'll iterate the relation.
Optimization 9: Soft-Reference Lazy Cache for Reclaimable Values¶
Problem¶
A large lazily-built index is held forever, even under memory pressure.
Optimized — recomputable via SoftReference¶
private SoftReference<Index> ref = new SoftReference<>(null);
public synchronized Index index() {
Index i = ref.get();
if (i == null) { i = build(); ref = new SoftReference<>(i); }
return i;
}
Effect: the GC may reclaim the index under pressure; the next access rebuilds it. This is lazy init blended with memoization eviction.
Tradeoff¶
SoftReference survives until near-OOM, which can raise GC pressure. Profile before adopting; a size-bounded cache is often better.
Optimization 10: Avoid Re-Lazy-Initializing Per Instance — Share a Singleton¶
Slow¶
class Validator {
private Pattern p;
Pattern pattern() { if (p == null) p = Pattern.compile(REGEX); return p; }
}
// thousands of Validators each compile their own copy of the SAME regex
Optimized — hoist to a shared, lazily-built constant¶
class Validator {
private static final class Holder { static final Pattern P = Pattern.compile(REGEX); }
Pattern pattern() { return Holder.P; } // compiled once for ALL instances
}
Lesson¶
If the lazily-built value is identical across instances, lazy-init it once at class level (holder idiom), not once per instance.
Optimization Tips¶
How to find lazy-init problems¶
- Profile the first-access spike. Trace spans around suspected lazy accessors reveal hidden I/O.
- Count queries in tests to catch ORM N+1.
- Check the steady-state read cost. A
synchronizedgetter orlru_cacheon a hot path shows up in CPU profiles. - Run
go test -race/ stress concurrent first-access to expose unsafe publication.
Optimization checklist¶
- Defer genuinely expensive, often-unused values; keep cheap/always-used ones eager.
- Use the holder idiom for static lazy singletons (lock-free reads).
- Use
volatileDCL orAtomicReferencefor per-instance lazy fields. - Use
sync.Once/OnceValuein Go,cached_property/lock in Python. - Warm critical lazies at startup to move the spike off the request path.
- Eager-fetch known ORM iteration paths to kill N+1.
- Hoist instance-identical lazy values to a shared class-level constant.
Anti-optimizations¶
- ❌
synchronized/mutex on every access — locks forever for a one-time init. - ❌ Lazy-init a cheap value — the guard costs more than the work.
- ❌ DCL without
volatile— fast but broken (unsafe publication). - ❌
sync.Oncefor transient-failure init — caches the error permanently. - ❌ SoftReference everywhere — survives until near-OOM, worsening GC pressure.
Summary¶
Lazy-init optimization is about paying for what you use without making the access path slow or unsafe. The wins: skip construction for unused values, keep the steady-state read lock-free (holder idiom, sync.Once, cached_property), warm critical values at startup to hide the spike, and eager-fetch ORM paths to avoid N+1. The pattern itself is rarely the bottleneck — the thread-safety mechanism and hidden I/O are where the real costs hide.
Lazy Initialization roadmap complete. All 8 files: junior · middle · senior · professional · interview · tasks · find-bug · optimize.
Next: Memoization & Caching — the keyed generalization of lazy init.
In this topic