Kotlin Delegation

Delegation is a design pattern where one object forwards responsibilities to another. Instead of inheriting behavior, a class delegates to a helper object. Kotlin supports delegation natively with the by keyword — both for classes and for properties — eliminating boilerplate forwarding code.

The Problem: Boilerplate Forwarding

interface Printer {
    fun print(doc: String)
    fun scan(): String
}

class RealPrinter : Printer {
    override fun print(doc: String) = println("Printing: $doc")
    override fun scan() = "Scanned document"
}

// Without delegation — must manually forward every function:
class PrinterProxy(private val real: RealPrinter) : Printer {
    override fun print(doc: String) = real.print(doc)   // boilerplate
    override fun scan(): String = real.scan()            // boilerplate
}

Class Delegation with by

// With delegation — Kotlin generates all forwarding automatically:
class LoggingPrinter(private val real: RealPrinter) : Printer by real {
    // Only override what you want to customize
    override fun print(doc: String) {
        println("[LOG] Document sent to printer")
        real.print(doc)
    }
    // scan() is automatically forwarded to real.scan()
}

fun main() {
    val base    = RealPrinter()
    val logging = LoggingPrinter(base)

    logging.print("Report.pdf")  // logs + prints
    println(logging.scan())      // forwarded directly to RealPrinter
}

Output:

[LOG] Document sent to printer
Printing: Report.pdf
Scanned document

Delegation Diagram


   LoggingPrinter
  ┌───────────────────────────────┐
  │  implements Printer           │
  │  overrides print() ← custom   │
  │  scan()  ─────────────────────┼──▶ RealPrinter.scan()
  └───────────────────────────────┘   (forwarded by 'by')

The 'by real' clause tells Kotlin:
"For any Printer method I don't override, call real's version."

Multiple Interface Delegation

interface Logger {
    fun log(msg: String)
}

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

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

class SimpleCache : Cache {
    private val store = mutableMapOf()
    override fun cache(key: String, value: String) { store[key] = value }
    override fun get(key: String) = store[key]
}

// DataService delegates Logger to ConsoleLogger and Cache to SimpleCache
class DataService(
    logger: Logger = ConsoleLogger(),
    cache: Cache   = SimpleCache()
) : Logger by logger, Cache by cache {

    fun fetchUser(id: String): String {
        val cached = get(id)
        if (cached != null) {
            log("Cache hit for $id")
            return cached
        }
        log("Fetching user $id from server")
        val user = "User_$id"
        cache(id, user)
        return user
    }
}

fun main() {
    val service = DataService()
    println(service.fetchUser("101"))
    println(service.fetchUser("101"))   // second call is cached
}

Output:

Fetching user 101 from server
User_101
Cache hit for 101
User_101

Property Delegation with by

Properties can also delegate their get/set logic to another object. This is how lazy and observable work (covered in the next topic):

class Delegate {
    operator fun getValue(thisRef: Any?, property: kotlin.reflect.KProperty<*>): String {
        return "Value from delegate for '${property.name}'"
    }

    operator fun setValue(thisRef: Any?, property: kotlin.reflect.KProperty<*>, value: String) {
        println("Setting '${property.name}' to '$value'")
    }
}

class Example {
    var myProp: String by Delegate()
}

fun main() {
    val ex = Example()
    println(ex.myProp)        // calls getValue
    ex.myProp = "Hello"       // calls setValue
}

Output:

Value from delegate for 'myProp'
Setting 'myProp' to 'Hello'

Practical Example: Feature Flag Service

interface FeatureFlags {
    fun isEnabled(feature: String): Boolean
    fun enable(feature: String)
    fun disable(feature: String)
}

class InMemoryFlags : FeatureFlags {
    private val flags = mutableSetOf()
    override fun isEnabled(feature: String) = feature in flags
    override fun enable(feature: String)  { flags.add(feature) }
    override fun disable(feature: String) { flags.remove(feature) }
}

class AuditedFlags(inner: InMemoryFlags) : FeatureFlags by inner {
    override fun enable(feature: String) {
        println("[AUDIT] Feature enabled: $feature")
        inner.enable(feature)
    }
    override fun disable(feature: String) {
        println("[AUDIT] Feature disabled: $feature")
        inner.disable(feature)
    }
}

fun main() {
    val flags = AuditedFlags(InMemoryFlags())
    flags.enable("dark_mode")
    flags.enable("beta_ui")
    println("dark_mode: ${flags.isEnabled("dark_mode")}")
    flags.disable("beta_ui")
}

Leave a Comment

Your email address will not be published. Required fields are marked *