In the previous tutorial, you learned about error handling. Now let’s learn about delegation. Delegation is a design pattern where an object hands off work to another object. Kotlin has built-in support for delegation using the by keyword.

In this tutorial, you will learn:

  • by lazy — lazy initialization
  • Delegates.observable — react to changes
  • Delegates.vetoable — reject invalid changes
  • Map delegation — properties from a map
  • Custom delegates — build your own
  • Class delegation — delegate interface implementation

by lazy

by lazy creates a property that is computed only once, the first time you access it. After that, the cached value is returned. This is useful for expensive initialization.

class DatabaseConnection {
    val connection: String by lazy {
        println("Connecting to database...")
        "Connected to PostgreSQL"
    }
}

val db = DatabaseConnection()
println("Object created")

println(db.connection) // Triggers lazy init, prints "Connecting..."
println(db.connection) // Uses cached value, no "Connecting..." printed

Output:

Object created
Connecting to database...
Connected to PostgreSQL
Connected to PostgreSQL

The lambda runs only on the first access. Every access after that returns the same value.

Thread Safety

By default, lazy is thread-safe. It uses synchronization to ensure the value is computed only once, even if multiple threads access it at the same time.

If you know the property will only be accessed from one thread, you can use LazyThreadSafetyMode.NONE for better performance:

val fastLazy: String by lazy(LazyThreadSafetyMode.NONE) {
    "Computed value"
}

When to Use lazy

  • Expensive computations (database connections, file loading)
  • Properties that might never be accessed
  • Properties that depend on other properties being set first
  • Singleton-style initialization
val expensiveValue: String by lazy {
    println("Computing...")
    "Result of expensive computation"
}

Delegates.observable

Delegates.observable fires a callback every time the value changes. You get the old value and the new value.

import kotlin.properties.Delegates

class UserSettings {
    var theme: String by Delegates.observable("Light") { _, old, new ->
        println("Theme changed: $old -> $new")
    }

    var fontSize: Int by Delegates.observable(14) { _, old, new ->
        println("Font size changed: $old -> $new")
    }
}

val settings = UserSettings()
settings.theme = "Dark"  // Theme changed: Light -> Dark
settings.theme = "Blue"  // Theme changed: Dark -> Blue
settings.fontSize = 18   // Font size changed: 14 -> 18

The first parameter _ is the property reference. We do not need it in most cases.

The callback receives three parameters:

  1. property — the property being changed (usually ignored with _)
  2. old — the previous value
  3. new — the new value

Observable is useful for:

  • Logging property changes
  • Updating the UI when data changes
  • Syncing state between components
  • Tracking what changed in a form

Delegates.vetoable

Delegates.vetoable fires a callback before the value changes. Return true to accept the change, false to reject it.

class Player {
    // Health cannot go below 0
    var health: Int by Delegates.vetoable(100) { _, _, new ->
        new >= 0
    }

    // Score can only increase
    var score: Int by Delegates.vetoable(0) { _, old, new ->
        new >= old
    }

    // Name must not be blank
    var name: String by Delegates.vetoable("Player") { _, _, new ->
        new.isNotBlank()
    }
}

Usage:

val player = Player()

player.health = 80
println(player.health) // 80

player.health = -10  // Rejected!
println(player.health) // Still 80

player.score = 100
println(player.score) // 100

player.score = 50  // Rejected — can only increase
println(player.score) // Still 100

player.name = "Alex"
println(player.name) // Alex

player.name = ""  // Rejected — blank
println(player.name) // Still Alex

Vetoable is useful for:

  • Validation rules on properties
  • Preventing invalid state changes
  • Range-restricted values

Map Delegation

You can delegate properties to a Map. The property name becomes the map key. This is useful for parsing JSON or configuration data.

Read-Only Map

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

val userData = mapOf(
    "name" to "Alex",
    "age" to 25,
    "email" to "alex@mail.com"
)

val user = UserFromMap(userData)
println(user.name)  // Alex
println(user.age)   // 25
println(user.email) // alex@mail.com

Mutable Map

With a mutable map, you can also write values:

class MutableConfig(map: MutableMap<String, Any?>) {
    var host: String by map
    var port: Int by map
    var debug: Boolean by map
}

val configData = mutableMapOf<String, Any?>(
    "host" to "localhost",
    "port" to 8080,
    "debug" to false
)

val config = MutableConfig(configData)
println(config.host) // localhost

config.host = "api.example.com"
println(config.host)              // api.example.com
println(configData["host"])       // api.example.com — map updated too!

Changing the property updates the map, and vice versa. They share the same data.

Custom Delegates

Create your own delegate by implementing ReadWriteProperty. You need getValue() and setValue().

TrimmedString — Always Trims Whitespace

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

class TrimmedString(private var value: String = "")
    : ReadWriteProperty<Any?, String> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return value
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        this.value = value.trim()
    }
}

ClampedInt — Keeps Value in a Range

class ClampedInt(
    private var value: Int,
    private val min: Int,
    private val max: Int
) : ReadWriteProperty<Any?, Int> {

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

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        this.value = value.coerceIn(min, max)
    }
}

LoggedProperty — Logs Every Access

class LoggedProperty<T>(private var value: T)
    : ReadWriteProperty<Any?, T> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        println("Reading ${property.name}: $value")
        return value
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        println("Writing ${property.name}: ${this.value} -> $value")
        this.value = value
    }
}

Using Custom Delegates

class FormData {
    var username: String by TrimmedString()
    var bio: String by TrimmedString()
    var volume: Int by ClampedInt(50, 0, 100)
    var brightness: Int by ClampedInt(75, 0, 100)
}

val form = FormData()

form.username = "  Alex  "
println(form.username) // "Alex" — trimmed!

form.volume = 150
println(form.volume)   // 100 — clamped to max

form.volume = -10
println(form.volume)   // 0 — clamped to min

Class Delegation

Class delegation lets a class delegate interface implementation to another object. The compiler generates all the forwarding methods for you.

Basic Class Delegation

interface Logger {
    fun log(message: String)
    fun error(message: String)
}

class ConsoleLogger : Logger {
    override fun log(message: String) = println("[LOG] $message")
    override fun error(message: String) = println("[ERROR] $message")
}

class Service(logger: Logger) : Logger by logger {
    fun doWork() {
        log("Starting work...")
        // ... do something
        log("Work completed")
    }
}

val service = Service(ConsoleLogger())
service.doWork()
// [LOG] Starting work...
// [LOG] Work completed

The Service class implements Logger by delegating all calls to the logger object. You do not need to write override fun log(...) — the compiler does it.

Override Specific Methods

You can still override specific methods while delegating the rest:

class PrefixedLogger(private val prefix: String, logger: Logger)
    : Logger by logger {

    override fun log(message: String) {
        println("[$prefix] $message") // Custom implementation
    }
    // error() is still delegated to the original logger
}

val prefixed = PrefixedLogger("APP", ConsoleLogger())
prefixed.log("Hello")   // [APP] Hello — uses override
prefixed.error("Oops")  // [ERROR] Oops — uses delegate

Multiple Delegation

A class can delegate multiple interfaces:

interface Cache {
    fun get(key: String): String?
    fun put(key: String, value: String)
}

class InMemoryCache : Cache {
    private val store = mutableMapOf<String, String>()
    override fun get(key: String): String? = store[key]
    override fun put(key: String, value: String) { store[key] = value }
}

class CachedService(
    logger: Logger,
    cache: Cache
) : Logger by logger, Cache by cache {

    fun fetchData(key: String): String {
        val cached = get(key)
        if (cached != null) {
            log("Cache hit: $key")
            return cached
        }
        log("Cache miss: $key")
        val data = "Data for $key"
        put(key, data)
        return data
    }
}

Usage:

val service = CachedService(ConsoleLogger(), InMemoryCache())
println(service.fetchData("users"))  // Cache miss, returns data
println(service.fetchData("users"))  // Cache hit, returns cached data

Delegation vs Inheritance

Consider this example without delegation:

// Without delegation — must override all methods manually
class ServiceWithoutDelegation(private val logger: Logger) : Logger {
    override fun log(message: String) = logger.log(message)
    override fun error(message: String) = logger.error(message)

    fun doWork() {
        log("Working...")
    }
}

With delegation, you skip all the boilerplate:

// With delegation — compiler generates forwarding methods
class ServiceWithDelegation(logger: Logger) : Logger by logger {
    fun doWork() {
        log("Working...")
    }
}

If Logger had 10 methods, you would need to write 10 override methods without delegation. With delegation, you write zero.

Delegation in Real Projects

Delegation is used widely in Android development and other Kotlin frameworks. For example:

// ViewModel that delegates to a repository
class UserViewModel(
    private val repository: Repository<User>
) {
    // Can delegate specific behavior
    fun getUsers() = repository.getAll()
}

// Adapter pattern with delegation
interface OldApi {
    fun fetchData(): String
}

interface NewApi {
    fun getData(): String
}

class OldApiImpl : OldApi {
    override fun fetchData(): String = "Old data"
}

class ApiAdapter(oldApi: OldApi) : NewApi {
    private val delegate = oldApi
    override fun getData(): String = delegate.fetchData()
}

Why Use Class Delegation?

  • Composition over inheritance — delegate instead of extending
  • Decoupling — swap implementations easily
  • Less boilerplate — no need to write forwarding methods

Summary

Delegation TypeSyntaxUse for
Lazyby lazy { }Expensive one-time init
ObservableDelegates.observable()React to changes
VetoableDelegates.vetoable()Validate changes
Mapby mapProperties from map/JSON
Customby CustomDelegate()Custom logic
Class: Interface by implForward interface calls

Key takeaways:

  • by lazy computes a value once and caches it
  • Delegates.observable notifies you when a value changes
  • Delegates.vetoable lets you reject invalid changes
  • Map delegation is great for parsing configuration data
  • Custom delegates let you add any logic to property access
  • Class delegation avoids inheritance and reduces boilerplate

Source Code

You can find the complete source code for this tutorial on GitHub:

KT-15 Source Code on GitHub

What’s Next?

In this tutorial, you learned about lazy initialization, observable and vetoable delegates, map delegation, custom delegates, and class delegation.

In the next tutorial, you will learn about sequences — lazy collections that can improve performance when working with large data sets.


This is part 15 of the Kotlin Tutorial series. Check out Part 14: Error Handling if you missed it. Need a quick reference? See the Kotlin Cheat Sheet.