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 bothvalandvardelegated properties.operator fun setValue(thisRef: R, property: KProperty<*>, value: T)— required only forvardelegated 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>
valorvar—valonly needs the delegate to implementgetValue;varneeds bothgetValueandsetValue.<Type>— usually inferable from the delegate’sgetValuereturn 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 correctgetValue/setValueoperator functions: a stdlib helper likelazy { ... }, an instance of your own delegate class, or aMap.
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
- The compiler sees
byand checks that the right-hand expression’s type has a compatiblegetValue(andsetValue, forvar) operator function — either declared as a member or available as an extension. If not, compilation fails immediately. - 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. - Every read of the property is rewritten by the compiler into
delegateField.getValue(this, propertyMetadata). - Every write (for a
var) is rewritten intodelegateField.setValue(this, propertyMetadata, newValue). - 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
lazyfor 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.observableinstead of writing a custom setter with manual logging or notification code — it is shorter and clearly signals “something reacts to this change.” - Use
Delegates.vetoablewhen avarneeds validation that can reject bad values while keeping the old one; remember its lambda must return aBoolean. - Prefer
Delegates.notNull<T>()over a nullable type plus!!when a non-nullvargenuinely 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
- 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. - Write a custom delegate class
TrimmedStringwhosesetValuetrims leading/trailing whitespace before storing the value, and whosegetValuelogs every read. Use it for avar username: String by TrimmedString()property, and verify that assigning" alice "results in reading back"alice". - Given
val config = mapOf("host" to "localhost", "port" to 8080), write a classServerConfig(map: Map<String, Any?>)withval host: Stringandval port: Intdelegated to the map, then print both fields.
Summary
- Property delegation uses the
bykeyword to hand a property’sget/setlogic to a separate delegate object instead of a plain backing field. - A delegate must provide an
operator fun getValue(...)forval, and additionally anoperator fun setValue(...)forvar. - The standard library ships
lazy,Delegates.observable,Delegates.vetoable,Delegates.notNull, andMap-backed delegation, covering the most common patterns without custom code. lazyonly works withval, 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 someDelegateon a property) with class delegation (: Interface by instanceon a class) — they share a keyword but solve different problems.
