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 lazyfor deferred immutable initialisation. - Use
Delegates.observablefor callback-on-change patterns. - Use
Delegates.vetoablefor validation with rejection. - Use
Delegates.notNullforvarnon-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
provideDelegatefor elaborate per-property setup. - Document delegated properties — admit substantial non-obvious behaviour.
- Use
lateinitoverDelegates.notNullfor non-primitivevarproperties.
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.