The by Keyword: Delegation Without the Boilerplate


“Favor composition over inheritance” is advice everyone repeats and few enjoy following, because composition in Java means forwarding methods by hand — a wrapper class full of one-line methods that just call through to the thing inside. Kotlin has a keyword that writes those for you: by. The same keyword also lets a property hand its get-and-set logic to a separate object, which is how features like lazy initialization become one word.

This post follows visibility, and revisits the by you may have glimpsed in the classes post.

Class delegation

Say you want a list that logs every addition. By inheritance you’d subclass and hope you overrode every relevant method. By composition you’d wrap a real list and forward dozens of methods. Kotlin’s by forwards them automatically: implement an interface by an instance, and every method you don’t override is delegated to that instance.

class LoggingList<T>(
    private val inner: MutableList<T> = mutableListOf()
) : MutableList<T> by inner {
    override fun add(element: T): Boolean {
        println("adding $element")
        return inner.add(element)
    }
}

LoggingList is a full MutableListsize, get, remove, all of it — yet the only method written is the one that changes behavior. Everything else is generated to call inner. That’s composition with the ergonomics of inheritance, and unlike inheritance it works even when the type is final.

Delegated properties

The same by works on a property: instead of giving it a backing field, you hand its get/set to a delegate object that runs the logic. The standard library ships the delegates you’ll actually use.

lazy

by lazy { } computes a value the first time it’s read and caches it forever after. The block runs at most once:

val config: Config by lazy {
    println("loading config")     // runs only on first access
    loadConfigFromDisk()
}

Perfect for expensive values you might never need. It’s also thread-safe by default. Contrast it with lateinit, which is for a non-null var you’ll assign to later; lazy is for a val that computes itself on demand.

observable

Delegates.observable fires a callback after every assignment — useful for change tracking without hand-writing a custom setter:

import kotlin.properties.Delegates

var name: String by Delegates.observable("<unset>") { _, old, new ->
    println("name changed: $old -> $new")
}

There’s a vetoable cousin that can reject a change by returning false from the handler.

Writing your own delegate

The built-ins cover most needs, but a delegate is no more than an object with getValue (and, for a var, setValue) operator functions. by wires the property’s reads and writes to those. Once you’ve seen one, the magic evaporates:

class UpperCase {
    private var stored = ""
    operator fun getValue(thisRef: Any?, property: KProperty<*>) = stored
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        stored = value.uppercase()
    }
}

class User {
    var name: String by UpperCase()
}

User().apply { name = "ada" }.name    // "ADA" — transformed on write

The property argument carries the property’s metadata — its name, for instance — which is what lets one delegate serve many properties and still tell them apart.

A map-backed delegate

A property can delegate to a Map, reading its value by the property’s own name as the key — handy for dynamic data like a parsed JSON object. The standard library supplies the getValue for Map, so this needs no custom delegate:

class Config(private val source: Map<String, Any?>) {
    val host: String by source
    val port: Int by source
}

Config(mapOf("host" to "localhost", "port" to 8080)).host   // "localhost"

lateinit vs lazy, side by side

These two solve adjacent problems and it’s worth keeping them straight:

  • lateinit var — a non-null var you assign later, from outside. No caching, no default; just a promise to set it before reading.
  • val ... by lazy { } — a val that computes itself on first read and caches the result. You never assign it.

Reach for lateinit when something external (a framework, a setup method) provides the value; reach for lazy when the value can compute itself but you’d rather defer the cost until it’s needed.

Final thoughts

Both halves of by attack the same enemy: boilerplate that exists only to forward work somewhere else. Class delegation generates the forwarding methods that make composition practical; property delegation extracts get/set logic into a reusable object so lazy and observable become a single word at the declaration site. When you catch yourself writing pass-through code, there’s usually a by that erases it.

Next, the other place Kotlin smooths a notoriously rough Java feature: generics, and variance without the wildcards.

Practice: reinforce this with the companion workbook — short, click-to-reveal problems.