Lazy Initialization
Lazy Initialization defers creating an object or value until first use, saving resources and speeding startup. Thread safety and a first-access latency spike are its main trade-offs.
Lazy Initialization defers creating an object, computing a value, or acquiring a resource until the moment it is first required. It solves the waste of paying upfront for things that may never be used and the slow startup caused by eagerly building everything at launch. The complement, eager initialization, builds everything immediately.
How It Works
The value is stored in a field that starts empty (null, sentinel, or an "uninitialized" flag). On first access, the accessor checks whether the value exists; if not, it creates and caches it, then returns it. Subsequent accesses return the cached value directly. This check-create-cache logic lives behind a getter so callers are unaware of the deferral.
Thread safety is the central concern. If multiple threads can hit an uninitialized field, naive lazy code may initialize twice or expose a half-built object. Solutions include synchronized accessors, double-checked locking with a volatile field, atomic compare-and-set, or language facilities such as Java's holder idiom, C#'s Lazy<T>, Kotlin's by lazy, and Swift's lazy var. A lazy holder that itself is thread-safe is far less error-prone than hand-rolled locking.
When to Use It
Use lazy initialization when a resource is expensive to create and not always needed, when it would otherwise slow application startup, or when you need to break initialization-order or circular-dependency problems. It underpins lazy-loading of data (ORMs loading related entities on access), memoization of computed results, and on-demand configuration.
Avoid it when the object is always used soon anyway — deferral then only adds branching and the risk of a latency spike on first access in a hot path. It can also make resource usage less predictable and complicate error handling, since creation failures surface at use time rather than startup.
Trade-offs
Laziness saves memory and CPU for unused resources and speeds startup. Combined with caching it gives memoization, turning repeated expensive computations into a single one.
The costs are concurrency complexity, a one-time latency hit on first access (a problem in tail-latency-sensitive systems and serverless cold starts), deferred error reporting, and less predictable resource consumption. Eager initialization trades these for higher fixed startup cost and is sometimes preferable when failures should be caught early.
Related Patterns
Lazy initialization frequently implements a lazily created Singleton. The Virtual Proxy pattern uses it to defer loading a heavy object until a method is actually called. Object Pool defers some creation similarly. Memoization is lazy initialization applied per-argument to function results.
Example
A report object exposes a getChartImage() accessor. Rendering the chart is expensive, and many users never open the chart tab. The image field is null until first requested; the first call renders and caches it, later calls return the cached image. In Kotlin this is simply val chartImage by lazy { renderChart() }, which is thread-safe by default and removes the manual null check entirely.