Delegation & Delegated Properties (by)

Reviewed & published by Brayan K

Delegation uses the by keyword to hand a class's interface or a property's get/set logic to another object, letting Kotlin generate the forwarding code for you.

Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

You'll learn class delegation (class A : I by impl), delegated properties (by lazy, observable, by map), and how to write a custom delegate with getValue/setValue.

What You'll Learn in This Lesson

1️⃣ Class Delegation: by an Implementation

With class A : I by impl, class A implements interface I by automatically forwarding every method to impl. You write zero boilerplate pass-through methods and override only the ones you want to change. This is Kotlin's built-in "favour composition over inheritance".

// CLASS DELEGATION: 'class A : I by impl' makes A implement interface I
// by forwarding every call to the object 'impl' — no boilerplate methods.
interface Printer {
    fun print(msg: String): String
}

class ConsolePrinter : Printer {
    override fun print(msg: String): String = "Printed: $msg"
}

// Loud delegates all Printer methods to 'base', overriding just one:
class LoudPrinter(base: Printer) : Printer by base {
    override fun print(msg: String): String = "Printed: ${msg.uppercase()}"
}

fun main() {
    val plain = ConsolePrinter()
    val loud = LoudPrinter(plain)
    println(plain.print("hi"))   // -> Printed: hi
    println(loud.print("hi"))    // -> Printed: HI
}

LoudPrinter inherits Printer's contract from base but supplies its own print — composition without a class hierarchy.

2️⃣ Delegated Properties: lazy & observable

A delegated property hands its get/set to a helper via by. by lazy computes a value once, on first read, then caches it. Delegates.observable fires a callback after every change. The standard library ships these so you don't write the plumbing.

import kotlin.properties.Delegates

// DELEGATED PROPERTIES: 'by' hands off get/set to a helper.
class Config {
    // by lazy: computed once, on first access, then cached.
    val token: String by lazy {
        println("computing token")
        "abc123"
    }

    // by Delegates.observable: runs a callback after every change.
    var level: Int by Delegates.observable(1) { _, old, new ->
        println("level $old -> $new")
    }
}

fun main() {
    val c = Config()
    println("before")
    println(c.token)      // first access computes it
    println(c.token)      // second access uses the cache (no recompute)

    c.level = 2
    c.level = 5
}

Notice "computing token" prints only once even though token is read twice — that's the lazy cache. And each assignment to level triggers the observer.

3️⃣ Writing a Custom Delegate

Any object can back a property if it provides getValue (and, for a var, setValue) operator functions. Kotlin routes every read and write through them, handing over the owning object and a KProperty. This hook lets you transform, validate, or log values automatically.

import kotlin.reflect.KProperty

// A CUSTOM DELEGATE: any class with getValue/setValue can back a property.
class UpperCaseDelegate {
    private var stored: String = ""
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String = stored
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        stored = value.uppercase()   // transform on the way in
    }
}

class Form {
    var code: String by UpperCaseDelegate()
}

fun main() {
    val f = Form()
    f.code = "hello"
    println(f.code)        // -> HELLO
    f.code = "abc"
    println(f.code)        // -> ABC
}

Every write to code is uppercased on the way in — the property itself looks completely ordinary at the call site.

Your turn. Fill in the ___, then run and compare.

// 🎯 YOUR TURN — replace the ___ then run main()
class Lazyish {
    // Compute this value only once, lazily, when first read.
    val greeting: String ___ lazy {   // 👉 put "by" in the blank
        "hello"
    }
}

fun main() {
    val x = Lazyish()
    println(x.greeting)   // ✅ Expected: hello
    println(x.greeting)   // ✅ Expected: hello   (uses the cached value)
}

Mini-Challenge: Map-Backed Properties

Back read-only properties with a Map using by map — each property reads the map entry matching its own name.

// 🎯 MINI-CHALLENGE: store properties in a map with 'by map'.
// Save as Settings.kt and run main().
//
// Kotlin can back read-only properties with a Map using 'by'.
//
// 1. Make class Settings(map: Map<String, Any?>) and inside it:
//      val name: String by map
//      val volume: Int   by map
//    (each property reads map["name"], map["volume"] by its own name)
// 2. Construct it with mapOf("name" to "Speaker", "volume" to 7).
// 3. Print name then volume.
//
// ✅ Expected output:
//    Speaker
//    7

fun main() {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Delegation

ConceptKotlin Syntax
Class delegationclass A : I by impl
Lazy propertyval x by lazy { ... }
Observablevar n by Delegates.observable(0) { ... }
Map-backedval name: String by map
Custom getteroperator fun getValue(...)
Custom setteroperator fun setValue(...)

🎉 Lesson Complete!

Practice quiz

What does 'class A : I by impl' do?

  • Makes A implement I by forwarding calls to impl
  • Makes A a subclass of impl
  • Prevents A from implementing I
  • Copies impl's source code into A

Answer: Makes A implement I by forwarding calls to impl. Class delegation makes A implement interface I by forwarding every call to impl.

In class delegation, what can you still do for specific methods?

  • Nothing, all methods are fixed
  • Delete them entirely
  • Override just the ones you want to change
  • Only rename them

Answer: Override just the ones you want to change. You override only the members you want to change; the rest are forwarded automatically.

What keyword introduces a delegated property?

  • with
  • as
  • is
  • by

Answer: by. The 'by' keyword hands a property's get/set logic to a delegate.

When is a 'by lazy' value computed?

  • On first access, then cached
  • When the object is constructed
  • Every time it is read
  • Never, until explicitly invoked

Answer: On first access, then cached. lazy computes on first read and caches the result for later reads.

What does Delegates.observable do?

  • Caches the value once
  • Makes the property read-only
  • Stores the value in a database
  • Runs a callback after every change to the property

Answer: Runs a callback after every change to the property. Delegates.observable fires a callback after each assignment.

What does a custom var delegate need?

  • operator getValue and setValue functions
  • Only a constructor
  • An iterator() function
  • A companion object

Answer: operator getValue and setValue functions. A var delegate needs operator fun getValue and operator fun setValue.

By default, how thread-safe is 'by lazy'?

  • It is never thread-safe
  • It computes the value at most once even under concurrent access
  • It recomputes per thread
  • It throws under concurrency

Answer: It computes the value at most once even under concurrent access. Default lazy is thread-safe, computing the value at most once even concurrently.

With 'val name: String by map', how is the value looked up?

  • By the map's first entry
  • By a hard-coded key
  • By the property's own name as the key
  • Randomly

Answer: By the property's own name as the key. Map-backed properties look up the entry whose key matches the property name.

Which arguments does a getValue operator receive?

  • Only the value
  • thisRef and a KProperty
  • Nothing
  • A list and an index

Answer: thisRef and a KProperty. getValue receives the owning object (thisRef) and a KProperty describing the property.

How does Delegates.vetoable differ from observable?

  • It can reject a change by returning false
  • It caches like lazy
  • It runs before construction
  • It cannot use a callback

Answer: It can reject a change by returning false. vetoable is like observable but can veto (reject) a change by returning false.

Continue this course

Frequently asked questions

What is class delegation and how does 'by' help?

Class delegation lets one class implement an interface by handing every call off to another object that already implements it. You write class Wrapper(base: I) : I by base, and Kotlin generates all the forwarding methods for you — you only override the few you want to change. It is the language-level answer to the 'favour composition over inheritance' advice: you get the interface of base without subclassing it, and without writing dozens of boilerplate pass-through methods.

What is a delegated property?

A delegated property uses the by keyword to hand its get (and for a var, its set) logic to a separate delegate object instead of storing the value directly. So val token by lazy { ... } means 'whenever someone reads token, ask the lazy delegate for the value'. The standard library ships several ready-made delegates — lazy, Delegates.observable, Delegates.vetoable, and map-backed properties — and you can write your own.

How is 'by lazy' different from a normal val?

A normal val is computed when the object is constructed. A val backed by lazy is not computed until the first time it is read, and the result is then cached so later reads reuse it without recomputing. This is ideal for expensive values you might not always need: the work is deferred until it is actually required and never repeated. By default lazy is also thread-safe, computing the value at most once even under concurrent access.

How do I write a custom delegate?

Provide a class with an operator fun getValue(thisRef, property) — and, for a var, an operator fun setValue(thisRef, property, value). When you declare a property with by YourDelegate(), Kotlin routes reads to getValue and writes to setValue, passing along the owning object and a KProperty describing the property. That hook lets your delegate transform, validate, log, or store the value however you like, as the UpperCaseDelegate example does by uppercasing on every write.