The by Keyword: Delegation Without the Boilerplate
Class delegation that makes composition as cheap as inheritance, delegated properties like lazy and observable, lazy's three thread-safety modes, and writing your own delegate with getValue/setValue.
“Favor composition over inheritance” is advice everyone repeats and few enjoy following, because composition in Java means forwarding methods by hand — a wrapper full of one-line methods that call through to the thing inside. Kotlin has a keyword that writes those for you: by. The same keyword lets a property hand its get-and-set logic to a separate object, which is how a feature like lazy initialization becomes one word. Everything here was run against Kotlin 2.4.10.
This chapter follows visibility and revisits the by you glimpsed in the classes chapter.
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 MutableList — size, 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. One caveat worth knowing: the generated methods forward to the stored delegate, so if inner’s own methods call each other internally, those internal calls bypass your override — delegation forwards the interface surface, it doesn’t rewire the delegate’s internals.
Delegated properties
The same by works on a property: instead of a backing field, you hand its get/set to a delegate object that runs the logic. The standard library ships the ones you’ll actually use.
lazy, and its three modes
by lazy { } computes a value on first 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. The detail most tutorials skip is that lazy takes a thread-safety mode, and the default is the cautious one:
LazyThreadSafetyMode.SYNCHRONIZED(the default) — locks so only one thread ever runs the initializer. Safe everywhere, with a synchronization cost.LazyThreadSafetyMode.PUBLICATION— several threads may race to initialize, but the first result published wins and is the one everyone sees. For idempotent, cheap initializers where you’d rather not lock.LazyThreadSafetyMode.NONE— no synchronization at all. Fastest, and only correct when the property is confined to a single thread. Using it across threads is undefined behavior.
val cache: Map<String, Int> by lazy(LazyThreadSafetyMode.NONE) { buildCache() }
Reach for NONE in single-threaded contexts (a UI thread, a confined object) where the default lock is pure overhead. Leave it at SYNCHRONIZED when in doubt.
observable and vetoable
Delegates.observable fires a callback after every assignment — change tracking without a hand-written setter:
import kotlin.properties.Delegates
var name: String by Delegates.observable("<unset>") { _, old, new ->
println("name changed: $old -> $new")
}
Delegates.vetoable runs before the assignment and can reject it by returning false from the handler, leaving the old value in place — validation that can actually veto.
Writing your own delegate
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:
import kotlin.reflect.KProperty
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 is a KProperty carrying the property’s metadata — its name, for one — which is what lets a single delegate serve many properties and still tell them apart (a validation delegate can put the property’s own name in its error message). There’s an optional provideDelegate operator too, run once when the delegate is created, useful for validating the property against the delegate at construction time.
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 getValue for Map, so this needs no custom delegate:
class Config(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
Two adjacent tools worth keeping straight:
lateinit var— a non-nullvaryou assign later, from outside. No caching, no default; just a promise to set it before reading, and an::prop.isInitializedcheck.val ... by lazy { }— avalthat computes itself on first read and caches. 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 and you’d rather defer the cost.
Final thoughts
Both halves of by attack the same enemy: boilerplate that exists only to forward work elsewhere. Class delegation generates the forwarding methods that make composition practical, even over final types; property delegation extracts get/set logic into a reusable object, so lazy and observable become a single word at the declaration — and lazy’s three modes let you trade its default lock for speed when you know the threading. 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.
Comments