Delegation Explained

KOTLIN › Types & Classes

Delegation is Kotlin's answer to 'favor composition over inheritance' baked directly into the language. Interviewers reach for it because it separates two things people often conflate: reusing an implementation by forwarding to another object, and reusing an implementation by extending a class. The by keyword on a class handles the first case with zero boilerplate.

interface Base { fun print() }

class BaseImpl(val x: Int) : Base {
    override fun print() = println(x)
}

class Derived(b: Base) : Base by b   // compiler generates: override fun print() = b.print()

Derived(BaseImpl(10)).print()   // 10

class Derived(b: Base) : Base by b tells the compiler that Derived implements Base, and every member of Base should forward to the object b. You write no manual overrides at all. Derived holds a Base internally instead of extending one, gets its full API for free, and can still override any individual member with a real implementation whenever it needs to diverge from straight forwarding.

That forwarding is real code generated once, at compile time, and it always calls the delegate's own method bodies, not whatever the deriving class does with the same member. So when the delegate's method reads one of its own properties, an override in the deriving class is invisible to it.

interface Base { val message: String; fun print() }

class BaseImpl : Base {
    override val message = "Base message"
    override fun print() = println(message)   // reads BaseImpl's own message
}

class Derived(b: Base) : Base by b {
    override val message = "Derived message"   // b.print() never sees this
}

Derived(BaseImpl()).print()   // prints "Base message"

Delegation isn't inheritance underneath. b is a separate object with its own state, and Derived overriding message only changes what derived.message returns when you call it directly. It has no effect on code running inside b's own methods. To change what gets printed, Derived would have to override print() itself, not just message.

Property delegation is a separate mechanism that reuses the same by keyword for a completely different purpose: instead of delegating a whole interface, you delegate the get and set of a single val or var. You write a small object with operator functions, and Kotlin calls them on every read or write instead of using a normal backing field. A val delegate needs getValue; a var delegate needs getValue and setValue.

import kotlin.reflect.KProperty

class Loud {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "loud ${property.name}!"
    }
}

val greeting: String by Loud()
println(greeting)   // loud greeting!

Both functions must be marked operator, or the compiler won't route by through them at all. The property: KProperty<*> parameter is what lets one delegate object serve any property it's attached to: property.name gives you the declared property's name without hardcoding it anywhere, which is exactly how a map-backed delegate finds the right key to look up.

lazy is the standard library's most-used property delegate, and it's a good example of the val/var rule in action. lazy { ... } returns a Lazy<T> object whose only operator function is getValue, there's no setValue on it at all. The first read runs your lambda and caches the result; every later read just returns the cached value, and the lambda body never runs again.

val config: String by lazy { loadConfig() }   // OK: val + Lazy<T>.getValue

// var config: String by lazy { loadConfig() }   // ERROR: Lazy<T> has no setValue

A var delegate needs both getValue and setValue supplied by the object on the right of by. Lazy<T> was written to supply only getValue, so pairing lazy with var fails to compile, it never gets as far as running.

lazy takes an optional LazyThreadSafetyMode argument that controls what happens if two threads reach the property's first read at the same time. Left unset, it defaults to SYNCHRONIZED.

val obj1 by lazy { ExpensiveObject() }                                   // SYNCHRONIZED (default)
val obj2 by lazy(LazyThreadSafetyMode.PUBLICATION) { ExpensiveObject() } // may race, first wins
val obj3 by lazy(LazyThreadSafetyMode.NONE) { ExpensiveObject() }       // no protection at all

SYNCHRONIZED puts a lock around initialization, so only one thread ever runs the lambda and the rest wait for its result. PUBLICATION allows multiple threads to run the lambda concurrently with no lock; whichever result is published first is the one that gets cached, and the others are simply discarded. NONE skips synchronization entirely, it's the fastest option but only safe when you can guarantee the property is never actually accessed from more than one thread.

kotlin.properties.Delegates bundles a few ready-made var delegates for common patterns, and observable/vetoable are the two you'll be asked to compare most. Both take an initial value and a lambda, but they run at different points relative to the assignment.

import kotlin.properties.Delegates

var name: String by Delegates.observable("initial") { prop, old, new ->
    println("${prop.name}: $old -> $new")   // runs AFTER assignment, can't reject
}

var score: Int by Delegates.vetoable(0) { _, old, new ->
    new >= old   // runs BEFORE assignment; false rejects and keeps the old value
}

observable's handler runs after the assignment has already happened, and it has no return value, it's purely for notification. vetoable's handler runs before the assignment and returns a Boolean: return false and the write never happens at all, the property silently keeps its old value as though nothing was assigned.

A Map can back a property directly: val name: String by map reads map["name"], using the property's declared name as the key. This is a common trick for turning loosely-typed data, like parsed JSON, into typed properties without writing manual lookups for each field.

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

val user = User(mapOf("name" to "Ada", "age" to 30))
println(user.name)   // "Ada", read from map["name"]

A plain read-only Map only supports val. Swap in a MutableMap and you also get var support: writes go through the map's own setValue, landing back in the map under the property's name key.

class Settings(val map: MutableMap<String, Any?>) {
    var theme: String by map
}

val s = Settings(mutableMapOf("theme" to "light"))
s.theme = "dark"
println(s.map["theme"])   // "dark"

One more standard delegate worth knowing: Delegates.notNull(). It gives you a non-null var whose value doesn't have to be supplied at construction time, similar in spirit to lateinit, but it also works with primitive types like Int, which lateinit can't handle.

var token: String by Delegates.notNull()

// println(token)   // throws IllegalStateException, no value set yet

token = "abc123"
println(token.length)   // 6, safe now that a value exists

Reading the property before you've assigned it throws an exception rather than silently returning a default or null. That's the whole point: it lets you declare a property whose real value only becomes available later, while still catching, loudly, any code path that tries to use it too early.

When you write your own delegate, you rarely have to declare bare operator fun getValue/setValue from scratch, the standard library already declares the right signatures as interfaces you can implement. ReadOnlyProperty<T, V> covers val delegates. ReadWriteProperty<T, V> extends it and adds setValue, so it covers var delegates.

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

class Logging<T>(private var value: T) : ReadWriteProperty<Any?, T> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): T = value
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        println("${property.name}: ${this.value} -> $value")
        this.value = value
    }
}

var name: String by Logging("Alice")
name = "Bea"   // prints: name: Alice -> Bea

Implementing the interface instead of hand-writing bare operator funs gets you compiler-checked overrides and makes the contract obvious at a glance. This is the idiomatic way to package a reusable custom delegate, including ones that validate or transform a value on write, like clamping it into a range or rejecting a bad value before it's stored.

Every delegated property is backed by a hidden field the compiler generates for you. For val bar: String by SomeDelegate(), it roughly generates:

private val `bar$delegate` = SomeDelegate()
val bar: String
    get() = `bar$delegate`.getValue(this, ::bar)

Every read or write of bar actually calls getValue or setValue on that hidden bar$delegate field, passing this and a KProperty reference to bar. That's the mechanism behind everything in this lesson, by isn't magic, it's a fixed rewrite the compiler performs once at compile time.

There's one more hook that runs even earlier than getValue: operator fun provideDelegate(thisRef, property), called exactly once, when the delegate is bound at declaration, before any getValue or setValue ever fires. It's the place to validate arguments or hand back a different delegate instance, and because it only runs once, it's also where you'd capture something like the property's name if you need it fixed at binding time rather than recomputed on every access.

class ResourceDelegate(val id: Int) {
    operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): ResourceDelegate {
        check(id > 0) { "invalid id for '${prop.name}'" }
        return this
    }
    operator fun getValue(thisRef: Any?, property: KProperty<*>) = id
}

Step back and the pattern across this whole lesson is the same one twice. Class delegation forwards an entire interface to an object you hold, so you get composition without writing forwarding code by hand. Property delegation forwards the get and set of one property to an object with getValue/setValue, so you get validation, caching, or logging without writing that boilerplate by hand either. In both cases by is doing the same job: taking behavior you'd otherwise write manually and routing it through an object that already knows how.

In an interview, the sentence worth having ready is: delegation lets you reuse implementation through composition instead of inheritance, at two levels, a whole interface via by on a class, or a single property's get and set via by on a val/var. The standard library already ships delegates for the common cases: lazy for one-time computation, observable/vetoable for reacting to or blocking writes, notNull for deferred non-null initialization, and map-backed delegation for loosely-typed data. Reach for a custom delegate, implementing ReadOnlyProperty or ReadWriteProperty, only when none of those fit, and remember the compiler is doing something concrete underneath all of it: a hidden $delegate field and calls to getValue/setValue, not something mysterious.

Back to Delegation