Polyglot
Languages Kotlin delegation
Kotlin § delegation

Property delegation

Property delegation is a substantial Kotlin feature — admits delegating get/set logic to a separate object via the by keyword. The standard library provides several conventional delegates: lazy (deferred initialisation), Delegates.observable (callback on change), Delegates.notNull (lateinit-style for primitives), Delegates.vetoable (callback may reject changes), Map delegation (read properties from a Map). Class delegation admits an interface implementation by delegation to another instance — substantial composition over inheritance. Custom delegates admit substantial flexibility through the getValue/setValue operators. The combination — substantial standard delegates, custom-delegate flexibility, class-level delegation, the by keyword unification — is the substance of Kotlin’s delegation surface.

Property delegation with by

The form: val/var name: Type by delegate:

class Service {
    val data: List<Item> by lazy {
        loadFromDisk()                              // computed on first access
    }
}

val s = Service()
s.data                                              // triggers computation
s.data                                              // cached

The by lazy { ... } admits the value being computed on first access; subsequent accesses return the cached value.

lazy

val expensiveValue: Result by lazy {
    println("computing...")
    expensiveComputation()
}

println(expensiveValue)                             // "computing..." then result
println(expensiveValue)                             // just the result (cached)

The lazy is thread-safe by default. For non-synchronised:

val value by lazy(LazyThreadSafetyMode.NONE) {
    /* ... */
}

The principal modes:

  • SYNCHRONIZED (default) — thread-safe; only one thread computes.
  • PUBLICATION — multiple threads may compute; the first to complete wins.
  • NONE — not thread-safe; substantial efficiency for known-single-threaded use.

Delegates.observable

Run a callback on each property change:

import kotlin.properties.Delegates

class Config {
    var theme: String by Delegates.observable("light") { property, old, new ->
        println("$property changed: $old -> $new")
    }
}

val c = Config()
c.theme = "dark"
// "theme changed: light -> dark"

The callback receives the property reference, the old value, and the new value.

Delegates.vetoable

Like observable, but the callback may reject the change:

class Settings {
    var maxConnections: Int by Delegates.vetoable(10) { _, _, new ->
        new > 0                                     // accept only positive
    }
}

val s = Settings()
s.maxConnections = 20                               // accepted (still 20)
s.maxConnections = -5                               // rejected (still 20)

The callback returns true to accept, false to reject.

Delegates.notNull

For non-nullable var properties initialised after construction (similar to lateinit, but for primitive types):

class Service {
    var connectionTimeout: Int by Delegates.notNull()

    fun configure(timeout: Int) {
        connectionTimeout = timeout
    }
}

val s = Service()
// s.connectionTimeout                              // throws — not initialised
s.configure(30)
s.connectionTimeout                                 // 30

The conventional alternative is lateinit var for non-primitive types — lateinit does not work with primitives.

Map delegation

A property may delegate to a Map (or MutableMap):

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

val user = User(mapOf(
    "name" to "Alice",
    "age" to 30,
    "email" to "alice@b.c"
))

println(user.name)                                  // "Alice"
println(user.age)                                   // 30

For mutable delegation:

class MutableUser(map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int by map
}

The pattern is conventional in JSON parsing and dynamic configuration.

Custom delegates

A custom delegate provides getValue (and optionally setValue):

import kotlin.reflect.KProperty

class StringValidator(private val regex: Regex) {
    private var value: String = ""

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String = value

    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
        require(newValue.matches(regex)) { "Invalid value for ${property.name}: $newValue" }
        value = newValue
    }
}

class Form {
    var email: String by StringValidator(Regex("[^@]+@[^@]+"))
    var phone: String by StringValidator(Regex("\\+?\\d+"))
}

val form = Form()
form.email = "alice@example.com"                   // OK
// form.email = "not-an-email"                     // throws

The mechanism admits substantial custom property behaviour.

ReadOnlyProperty and ReadWriteProperty

The standard interfaces:

interface ReadOnlyProperty<in T, out V> {
    operator fun getValue(thisRef: T, property: KProperty<*>): V
}

interface ReadWriteProperty<in T, V> {
    operator fun getValue(thisRef: T, property: KProperty<*>): V
    operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
}

Custom delegates may implement these for substantial integration.

import kotlin.properties.ReadOnlyProperty

class TaggedDelegate<T>(private val tag: String, private val computeValue: () -> T) :
    ReadOnlyProperty<Any?, T> {

    private var cache: T? = null

    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        if (cache == null) {
            println("[$tag] computing ${property.name}")
            cache = computeValue()
        }
        return cache!!
    }
}

class Service {
    val data by TaggedDelegate("svc") { loadData() }
}

provideDelegate

For substantial delegate-providing patterns:

class TaggedDelegateProvider(private val tag: String) {
    operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty<Any?, String> {
        println("[$tag] providing delegate for ${property.name}")
        return ReadOnlyProperty { _, _ -> "value of ${property.name}" }
    }
}

class Service {
    val data: String by TaggedDelegateProvider("svc")
}

The mechanism admits substantial customisation at delegation time.

Class delegation

Beyond properties, classes admit delegating interface implementations:

interface Logger {
    fun log(message: String)
}

class ConsoleLogger : Logger {
    override fun log(message: String) {
        println(message)
    }
}

class TaggedLogger(tag: String, private val delegate: Logger) : Logger by delegate {
    private val tag: String = tag
    // log() is automatically delegated to the underlying Logger
    // but can be overridden:
    override fun log(message: String) {
        delegate.log("[$tag] $message")
    }
}

val logger = TaggedLogger("APP", ConsoleLogger())
logger.log("started")                              // "[APP] started"

The form class X : Interface by other admits substantial composition over inheritance — admits adding behaviour to existing types without subclassing.

A more substantial example:

class TimestampedLogger(
    private val delegate: Logger
) : Logger by delegate {
    override fun log(message: String) {
        delegate.log("[${Instant.now()}] $message")
    }
}

class FilteredLogger(
    private val delegate: Logger,
    private val minLevel: Level
) : Logger by delegate {
    override fun log(message: String) {
        delegate.log(message)                       // can add filtering logic
    }
}

val logger = TimestampedLogger(FilteredLogger(ConsoleLogger(), Level.INFO))
logger.log("hello")                                 // "[2026-01-15...] hello"

The mechanism admits substantial decorator-style chaining.

Common patterns

Lazy initialisation

class Service {
    val database by lazy {
        Database.connect("jdbc:postgres://...")
    }

    val cache by lazy {
        Cache.create(maxSize = 1000)
    }
}

The lazy admits deferred expensive initialisation; conventional in dependency injection patterns.

Observable property

class FormViewModel {
    var name: String by Delegates.observable("") { _, old, new ->
        if (old != new) {
            validate(new)
            notifyChange("name")
        }
    }
}

Required initialisation via Map

class Config(props: Map<String, Any?>) {
    val host: String by props
    val port: Int by props
    val timeout: Int by props.withDefault { 30 }
}

val config = Config(mapOf(
    "host" to "localhost",
    "port" to 8080
    // timeout uses default
))

Validated property

class User {
    var email: String by Delegates.observable("") { _, _, new ->
        require(new.contains("@")) { "Invalid email: $new" }
    }
}

Custom delegate for typed configuration

class ConfigDelegate<T>(private val key: String, private val default: T) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        @Suppress("UNCHECKED_CAST")
        return Config.values[key] as? T ?: default
    }
}

class AppConfig {
    val timeout: Int by ConfigDelegate("timeout", 30)
    val debug: Boolean by ConfigDelegate("debug", false)
}

Class delegation for decorator

interface Cache<K, V> {
    fun get(key: K): V?
    fun set(key: K, value: V)
}

class LoggingCache<K, V>(
    private val delegate: Cache<K, V>
) : Cache<K, V> by delegate {
    override fun get(key: K): V? {
        val result = delegate.get(key)
        println("get($key) -> $result")
        return result
    }
}

Lazy for dependency injection

class UserService(
    private val database: Database = Service.database,
    private val notifier: NotificationService = Service.notifier
) {
    private val userRepository by lazy { UserRepository(database) }

    fun create(name: String): User {
        val user = User(generateId(), name)
        userRepository.save(user)
        notifier.notify(user)
        return user
    }
}

Mutable Map delegation

class UserBuilder(private val data: MutableMap<String, Any?>) {
    var name: String by data
    var age: Int by data
    var email: String? by data
}

val builder = UserBuilder(mutableMapOf())
builder.name = "Alice"
builder.age = 30
builder.email = "alice@b.c"

println(builder.data)                              // {name=Alice, age=30, email=alice@b.c}

View binding

class TextField {
    private var _value: String = ""

    var value: String
        get() = _value
        set(new) {
            if (_value != new) {
                _value = new
                listeners.forEach { it.onChange(new) }
            }
        }
}

// With observable:
class TextField {
    var value: String by Delegates.observable("") { _, _, new ->
        listeners.forEach { it.onChange(new) }
    }
}

Database column delegation

class DbColumn<T>(val name: String, val converter: (Any?) -> T) {
    operator fun getValue(thisRef: DatabaseRow, property: KProperty<*>): T {
        return converter(thisRef.getRaw(name))
    }
}

class User(row: DatabaseRow) {
    val id by DbColumn("id") { (it as Number).toLong() }
    val name by DbColumn("name") { it as String }
    val createdAt by DbColumn("created_at") { Instant.parse(it as String) }
}

Delegated implementation pattern

interface ReadableUser {
    val id: Int
    val name: String
}

interface WritableUser {
    var name: String
}

class User(
    override val id: Int,
    initialName: String
) : ReadableUser, WritableUser {
    override var name: String = initialName
}

class CachedUser(private val source: ReadableUser) : ReadableUser by source

class TrackedUser(private val source: ReadableUser) : ReadableUser by source {
    init { println("Created tracked view of ${source.id}") }
}

Custom thread-safe delegate

class SynchronizedDelegate<T>(initial: T) {
    private var value: T = initial
    private val lock = Any()

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T = synchronized(lock) {
        value
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) = synchronized(lock) {
        value = newValue
    }
}

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

Delegate for environment config

class EnvDelegate(private val key: String, private val default: String? = null) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String =
        System.getenv(key) ?: default ?: error("env var $key not set")
}

object Config {
    val databaseUrl by EnvDelegate("DATABASE_URL", "localhost:5432")
    val apiKey by EnvDelegate("API_KEY")
}

Multiple delegate composition

class Service {
    val cache by lazy { ... }                       // lazy initialisation
    var debug by Delegates.observable(false) { _, _, new ->
        println("debug = $new")
    }
    var timeout by Delegates.notNull<Int>()
    val config: Map<String, Any?> by lazy { loadConfig() }
}

A note on the conventional discipline

The contemporary Kotlin delegation advice:

  • Use by lazy for deferred immutable initialisation.
  • Use Delegates.observable for callback-on-change patterns.
  • Use Delegates.vetoable for validation with rejection.
  • Use Delegates.notNull for var non-nullable primitive initialisation.
  • Use Map delegation for dynamic property mapping.
  • Use class delegation for decorator-style composition.
  • Define custom delegates for substantial domain patterns.
  • Use provideDelegate for elaborate per-property setup.
  • Document delegated properties — admit substantial non-obvious behaviour.
  • Use lateinit over Delegates.notNull for non-primitive var properties.

The combination — substantial standard delegates (lazy, observable, vetoable, notNull), custom-delegate flexibility via the getValue/setValue operators, class-level interface delegation, the provideDelegate for elaborate setup, the by keyword unification — is the substance of Kotlin’s delegation surface. The discipline admits substantial declarative property behaviour and substantial composition patterns without subclassing.