Kotlin Delegated Properties, lazy and lateinit

Delegate property behavior, initialize values lazily and distinguish deferred assignment from computed initialization.

Lesson 29 of 43Advanced9 min read

A delegated property hands its reading or writing behavior to another object with by. The most familiar example is val report by lazy { buildReport() }, which waits until the first access to calculate a value and then retains it. Delegation can also support observed changes or a controlled property backed by another source.

The classes lesson introduces lateinit for later assignment. This reference distinguishes that mechanism from lazy initialization and explains the delegate protocol. You will choose an initialization policy from the value's lifetime instead of treating lazy and lateinit as interchangeable ways to postpone work.

Lazy computes a retained value on first access

By lazy takes an initializer lambda. Creating the owner does not run that lambda immediately. The first read initializes the value, and later reads use the stored result after successful initialization. A lazy property is commonly a val because its purpose is a single calculated value rather than externally assigned mutable storage.

Choose lazy when an optional expensive calculation may never be needed or when construction should not immediately perform that calculation. Do not use it to hide important validation failures until an unrelated property read. A required configuration value is often better validated at startup so failure occurs at an understandable boundary.

Lazy retains its result for the lazy object's lifetime. If the calculation depends on configuration that can change, retaining the first result may be incorrect. Make the dependency stable, recreate the owner when needed or use an explicit cache invalidation design. Delayed calculation and freshness are separate requirements.

Lateinit means someone must assign the value

Lateinit applies to supported non-null mutable properties that will be assigned after construction. Reading one before assignment throws an uninitialized-property exception. It does not supply an initializer function and does not automatically create the missing value. The owner or framework must establish the property before use.

It cannot be used for primitive-type properties such as Int in the same way as a supported reference property, nor for an ordinary val. If missing data is a real domain state, a nullable property may express it better. Lateinit should describe a guaranteed initialization lifecycle, not silence a requirement that nobody actually fulfills.

An initialization check can be useful inside the permitted property-reference scope, but adding checks everywhere may indicate that the guarantee is weak. Constructor injection is often cleaner for required dependencies because successful construction establishes them immediately. Framework-managed lifecycles sometimes justify deferred assignment, provided the lifecycle is explicit.

Observable and custom delegates

Delegates.observable receives an initial value and a callback for assignments, including the property metadata and old and new values. The callback observes the change after assignment. Delegates.vetoable instead lets a callback decide whether to accept the new value. Choose the one that matches notification versus rejection, not merely the one whose name sounds convenient.

A custom delegate supplies getValue for reads and setValue for mutable writes through the supported operator signatures. That makes property access concise, but it also hides implementation work behind normal-looking syntax. Avoid performing surprising network calls or expensive database queries on every read without a documented contract.

Property delegation is different from interface delegation in a class's supertype list. Both use by, but one controls a property's access and the other forwards interface members to another object. Keep the responsibilities separate even when a larger component uses both mechanisms.

Concurrency and initialization modes

The standard lazy factory offers synchronization modes. The default synchronized mode protects initialization on platforms where that mode provides the documented behavior. Publication allows competing initializations while publishing a chosen value, so an initializer may run more than once. None removes synchronization and requires an appropriate single-threaded ownership assumption.

Do not put a non-idempotent external action in a publication-mode initializer, because more than one initialization attempt can occur. Even synchronized lazy does not make the resulting mutable object thread-safe. It controls initialization, not every future mutation of the value.

The delegated property guide and lazy API document the protocol and modes. Platform behavior matters, especially outside the JVM. Start with the default mode unless a measured need and clear ownership contract justify another one.

Kotlin Delegated Properties, lazy and lateinit Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Observe initialization once

The printed marker makes evaluation timing visible. The second read does not repeat the calculation. This deterministic example uses no file or network work, so the lesson isolates initialization policy rather than relying on an external service that could fail or change.

Kotlin
class Catalog {
    val summary: String by lazy {
        println("building summary")
        "3 products"
    }
}

fun main() {
    val catalog = Catalog()
    println("created")
    println(catalog.summary)
    println(catalog.summary)
}
Output
created
building summary
3 products
3 products

Kotlin Delegated Properties, lazy and lateinit in Real Projects

Worked exercise: notify a stock change

Build a small stock label with an observed quantity. The callback should print the transition, and the main program should show the final value. This is a notification demonstration; it does not reject negative values. A production stock object should validate through an explicit method or an appropriate veto policy.

Kotlin
import kotlin.properties.Delegates

class StockLabel {
    var quantity: Int by Delegates.observable(2) { _, old, new ->
        println("stock:$old->$new")
    }
}

fun main() {
    val label = StockLabel()
    label.quantity = 5
    println("current:${label.quantity}")
}
Output
stock:2->5
current:5

The initial value is established without a change callback from this assignment sequence. The later assignment invokes the observer. Try assigning the same value and decide whether your interface should suppress redundant notifications; the generic delegate's assignment callback is not automatically a domain-level change detector. Keep observers small so property writes do not unexpectedly trigger a large chain of side effects.

Kotlin Delegated Properties, lazy and lateinit FAQ

When should I use lazy instead of lateinit?

Use lazy when the owner knows how to calculate a value and should do so on first access. Use lateinit when another lifecycle step must assign a supported mutable non-null property. Lazy has an initializer and retains its successful result; lateinit has an assignment obligation and throws if read too early. Constructor initialization remains a strong choice for required dependencies. Choose based on who owns initialization, whether absence is legitimate and how failures should be reported.

Does lazy make the resulting object thread-safe?

No. Its synchronization policy concerns initialization and publication of the retained value. A mutable list or service created by lazy can still require its own synchronization for later operations. Also check the chosen lazy mode and platform contract rather than assuming every mode serializes initialization. Treat the initialization boundary and the value's ongoing mutation boundary separately. This distinction prevents a protected first construction from being mistaken for protection of an entire shared subsystem.

Can an observable delegate reject an invalid assignment?

Observable is designed to notify after an assignment, not to veto it. Vetoable provides an acceptance callback when that fits the requirement. For substantial domain rules, an explicit method can be clearer because it can return a result, explain a failure and coordinate other state. Do not make a callback silently reject values while callers assume success unless that policy is documented. Notification and validation are related responsibilities but should not be confused.

Should every expensive property become lazy?

No. Lazy can avoid unused work, but it can also delay errors and retain stale results. Consider whether the value must be validated at construction, whether its dependencies change and whether callers understand the cost of the first read. An explicit load or calculate method can provide clearer error handling for external I/O. Use lazy for a stable, understandable deferred calculation rather than as a blanket optimization for any operation that happens to take time.

Kotlin Delegated Properties, lazy and lateinit Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

A service is required for every valid instance and is available at construction. Which approach is usually clearest?

Kotlin Delegated Properties, lazy and lateinit Quiz

2 questions to check what you learned.

  1. When does a normal lazy initializer run?

    • Every time the owner is constructed, before any access
    • On the first value access, with a successful result retained
    • Only when the program exits
  2. Which delegate is intended to accept or reject a proposed assignment?

    • Delegates.vetoable
    • Delegates.observable as an automatic validator
    • A package import