Property Delegation

Kotlin lets a property hand off its get (and, for a var, its set) logic to a separate object instead of writing that logic inline. This is called property delegation, and it is written with the by keyword: val name: String by someDelegate. Instead of copy-pasting the same caching, logging, validation, or change-notification code into every property that needs it, you write that behavior once in a small delegate class (or use one Kotlin already ships) and reuse it anywhere. It is one of the clearest examples of Kotlin being a “better Java”: the same lazy-initialization or observable-property pattern that requires boilerplate getters/setters (or an extra library) in Java is a one-line property declaration in Kotlin.

Overview: How Property Delegation Works

A normal Kotlin property already has a backing field plus a compiler-generated getter (and setter, if it is a var). Delegation changes where that logic lives: instead of a backing field, the compiler stores a reference to your delegate object, and rewrites every read of the property into a call to that delegate’s getValue operator function, and every write into a call to its setValue operator function.

For this to compile, the object after by must provide operator functions matching this shape:

  • operator fun getValue(thisRef: R, property: KProperty<*>): T — required for both val and var delegated properties.
  • operator fun setValue(thisRef: R, property: KProperty<*>, value: T) — required only for var delegated properties.

thisRef is the instance the property belongs to (or Any?/Nothing if you don’t care), and property is a lightweight KProperty<*> object the compiler generates that exposes metadata like property.name — this is not a full reflection lookup at runtime, it costs almost nothing. Because the delegate expression is evaluated once and stored as a hidden field on the containing class, a fresh delegate instance is created per declaration (so two properties each written as by LoggingDelegate(0) get two independent delegates, not one shared one).

Kotlin’s standard library ships several ready-made delegates in kotlin.properties.Delegates and kotlin itself so you rarely need to write getValue/setValue by hand:

Delegate Where val or var Purpose
lazy { ... } kotlin val only Runs the block once, on first access; caches the result forever after.
Delegates.observable(initial) { prop, old, new -> } kotlin.properties var Runs a callback after every successful assignment.
Delegates.vetoable(initial) { prop, old, new -> } kotlin.properties var Runs a callback before assignment; returning false silently keeps the old value.
Delegates.notNull<T>() kotlin.properties var A non-null property with no initial value; throws IllegalStateException if read before it is set.
by map / by mutableMap kotlin.collections (extension operators) val / var Reads (and optionally writes) a value from a Map, using the property name as the key.

One point of confusion worth heading off: the by keyword is also used for class delegation (class Widget(base: Base) : Base by base), which forwards an entire interface’s implementation to another object. That is a related but different feature — this lesson is specifically about delegating individual properties, not whole interfaces.

Syntax

val/var <propertyName>: <Type> by <delegateExpression>
  • val or varval only needs the delegate to implement getValue; var needs both getValue and setValue.
  • <Type> — usually inferable from the delegate’s getValue return type, but explicit types are common for readability.
  • by — the keyword that switches the property from “backing field” mode to “delegate” mode.
  • <delegateExpression> — any expression that evaluates to an object with the correct getValue/setValue operator functions: a stdlib helper like lazy { ... }, an instance of your own delegate class, or a Map.

Examples

Example 1: Lazy Initialization

lazy { ... } is the delegate you will reach for most often. The block only runs the first time the property is read; every access after that returns the cached value without re-running the block. This is ideal for expensive setup (parsing a file, opening a connection) that might never be needed.

fun main() {
    val message: String by lazy {
        println("Computing the value...")
        "Hello, Kotlin delegation!"
    }
    println("Before first access")
    println(message)
    println(message)
}

Output:

Before first access
Computing the value...
Hello, Kotlin delegation!
Hello, Kotlin delegation!

Notice the computing message prints only once, on the first println(message). The second access reuses the cached result. By default lazy is also thread-safe: it uses a lock so that if two threads read the property at the same time, the block still only executes once.

Example 2: Reacting to Changes with observable

Delegates.observable takes an initial value and a lambda that runs after every assignment, receiving the property, the old value, and the new value. It is useful for logging, validation side-effects, or notifying listeners (a hand-rolled “property changed” event).

import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("Unnamed") { property, old, new ->
        println("${'$'}{property.name} changed from ${'$'}old to ${'$'}new")
    }
}

fun main() {
    val user = User()
    user.name = "Alice"
    user.name = "Bob"
}

Output:

name changed from Unnamed to Alice
name changed from Alice to Bob

Each assignment to user.name triggers the callback automatically — no manual “fire an event” call is needed anywhere name is set.

Example 3: Writing a Custom Delegate

To see what the stdlib delegates are doing internally, here is a hand-written delegate that logs every read and write. It defines getValue and setValue directly.

import kotlin.reflect.KProperty

class LoggingDelegate(private var value: Int) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        println("Reading ${'$'}{property.name}: ${'$'}value")
        return value
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: Int) {
        println("Writing ${'$'}{property.name}: ${'$'}value -> ${'$'}newValue")
        value = newValue
    }
}

class Counter {
    var count: Int by LoggingDelegate(0)
}

fun main() {
    val counter = Counter()
    counter.count = 5
    println(counter.count)
    counter.count += 1
}

Output:

Writing count: 0 -> 5
Reading count: 5
5
Reading count: 5
Writing count: 5 -> 6

The last line, counter.count += 1, desugars to counter.count = counter.count + 1, which is why it triggers a read (to fetch the current value 5) followed by a write (to store 6).

Example 4: Delegating to a Map

The standard library provides getValue (and, for a MutableMap, setValue) as extension operator functions directly on Map<String, *>. This makes parsing loosely-typed data (JSON-like maps, configuration bundles) into strongly-typed properties very compact.

class User(map: Map<String, Any?>) {
    val name: String by map
    val age: Int by map
}

fun main() {
    val userMap = mapOf("name" to "Charlie", "age" to 30)
    val user = User(userMap)
    println("${'$'}{user.name} is ${'$'}{user.age} years old")
}

Output:

Charlie is 30 years old

Each property looks itself up in the map by its own name ("name", "age") and casts the result to its declared type. If a key is missing the call throws NoSuchElementException, and if the value’s runtime type doesn’t match the declared type it throws ClassCastException — so this pattern is best used when you control the shape of the map.

How It Works Step by Step

  1. The compiler sees by and checks that the right-hand expression’s type has a compatible getValue (and setValue, for var) operator function — either declared as a member or available as an extension. If not, compilation fails immediately.
  2. The delegate expression (lazy { ... }, LoggingDelegate(0), the map, etc.) is evaluated once, and the result is stored in a hidden, compiler-generated field alongside the class.
  3. Every read of the property is rewritten by the compiler into delegateField.getValue(this, propertyMetadata).
  4. Every write (for a var) is rewritten into delegateField.setValue(this, propertyMetadata, newValue).
  5. Because the delegate is a real, separate object, it can hold its own state (a cache, a lock, a backing map) independent of the class that declares the property — that is the entire point: the logic is reusable across any property or class.

Common Mistakes

Mistake 1: Using lazy with a var

lazy only ever provides getValue, so it cannot back a mutable property:

// Does NOT compile: "Property delegate must have a 'setValue(...)' method"
class Config {
    var setting: String by lazy { "default" }
}

If you need mutability with reactive behavior, reach for Delegates.observable instead:

import kotlin.properties.Delegates

class Config {
    var setting: String by Delegates.observable("default") { _, old, new ->
        println("setting changed from ${'$'}old to ${'$'}new")
    }
}

fun main() {
    val config = Config()
    println(config.setting)
    config.setting = "custom"
}

Output:

default
setting changed from default to custom

Mistake 2: Confusing = with by

lazy { ... } returns a Lazy<T> object, not a T. Assigning it with = instead of delegating it with by is a type mismatch:

// Does NOT compile: type mismatch, found Lazy<List<Int>>, required List<Int>
class Repository {
    val data: List<Int> = lazy { listOf(1, 2, 3) }
}

The fix is simply to use by, which tells the compiler to route reads through Lazy<T>‘s getValue rather than storing the Lazy object itself:

class Repository {
    val data: List<Int> by lazy { listOf(1, 2, 3) }
}

fun main() {
    val repo = Repository()
    println(repo.data)
}

Output:

[1, 2, 3]

Mistake 3: Expecting lazy to Recompute When Dependencies Change

lazy caches after the very first read, forever — it has no idea that a mutable list it read from was later changed:

class Report(private val items: MutableList<Int>) {
    val total: Int by lazy { items.sum() }
}

fun main() {
    val items = mutableListOf(1, 2, 3)
    val report = Report(items)
    println(report.total) // 6
    items.add(100)
    println(report.total) // still 6, NOT 106 -- surprising!
}

If the value genuinely needs to be recomputed on every access, don’t use lazy at all — use a plain custom getter instead:

class Report(private val items: MutableList<Int>) {
    val total: Int
        get() = items.sum()
}

fun main() {
    val items = mutableListOf(1, 2, 3)
    val report = Report(items)
    println(report.total)
    items.add(100)
    println(report.total)
}

Output:

6
106

Best Practices

  • Reach for lazy for expensive, one-time setup that a class might not always need — it also documents intent (“computed once, immutable after”) better than manual caching.
  • Use Delegates.observable instead of writing a custom setter with manual logging or notification code — it is shorter and clearly signals “something reacts to this change.”
  • Use Delegates.vetoable when a var needs validation that can reject bad values while keeping the old one; remember its lambda must return a Boolean.
  • Prefer Delegates.notNull<T>() over a nullable type plus !! when a non-null var genuinely can’t have a sensible default until later (e.g. dependency-injected fields) — it fails loudly and clearly if read too early.
  • Write a custom delegate class when the same get/set behavior (caching, validation, logging, thread-confinement) is duplicated across several unrelated properties or classes; otherwise a plain custom getter/setter is simpler and just as clear.
  • Remember each by SomeDelegate(...) declaration creates its own delegate instance — delegates do not implicitly share state across properties unless you explicitly pass them the same shared object.

Practice Exercises

  1. Declare var temperatureCelsius: Double by Delegates.vetoable(0.0) { _, _, new -> ... } so that any assignment below absolute zero (-273.15) is rejected and the property keeps its previous value. Print the old and new value whenever a change is accepted.
  2. Write a custom delegate class TrimmedString whose setValue trims leading/trailing whitespace before storing the value, and whose getValue logs every read. Use it for a var username: String by TrimmedString() property, and verify that assigning " alice " results in reading back "alice".
  3. Given val config = mapOf("host" to "localhost", "port" to 8080), write a class ServerConfig(map: Map<String, Any?>) with val host: String and val port: Int delegated to the map, then print both fields.

Summary

  • Property delegation uses the by keyword to hand a property’s get/set logic to a separate delegate object instead of a plain backing field.
  • A delegate must provide an operator fun getValue(...) for val, and additionally an operator fun setValue(...) for var.
  • The standard library ships lazy, Delegates.observable, Delegates.vetoable, Delegates.notNull, and Map-backed delegation, covering the most common patterns without custom code.
  • lazy only works with val, caches after the first read, and (by default) is thread-safe.
  • Custom delegate classes let you reuse behavior like logging, validation, or caching across many unrelated properties.
  • Don’t confuse property delegation (by someDelegate on a property) with class delegation (: Interface by instance on a class) — they share a keyword but solve different problems.