Class Delegation
Class delegation is Kotlin’s built-in support for the delegation design pattern: instead of a class inheriting behavior from a base class, it holds a reference to another object that already implements an interface, and forwards calls to it. Kotlin lets you express this in one word, by, and the compiler writes all the forwarding methods for you. This is how Kotlin encourages "favor composition over inheritance" without paying the usual boilerplate tax that pattern has in languages like Java.
Overview: What Class Delegation Is and How It Works
In classic object-oriented design, the Delegation pattern means: a class implements an interface not by providing its own logic, but by holding an instance of another type that already implements it, and forwarding every call to that instance. In Java, this means writing one wrapper method per interface member by hand, even when you only actually want to change one or two of them. Kotlin builds this pattern directly into the language with the by clause on a class declaration.
When you write class Derived(b: Base) : Base by b, you are telling the compiler: "Derived implements Base, and unless I say otherwise, every member of Base should simply call the matching member on b." Under the hood, the compiler stores the delegate object (b) in a hidden field and, for every abstract member of Base that Derived does not explicitly override, generates a method that calls that member on the stored field and returns its result. This happens entirely at compile time — there is no reflection involved, unlike some delegation patterns in other languages.
Two Kotlin rules make this work cleanly. First, delegation only works against an interface, not a class, because the compiler needs a contract (a fixed list of abstract members) to know what to forward; a class body’s implementation details are not enumerable in the same way. Second, the delegate expression is evaluated once, typically as a constructor parameter, and cached — it is not re-evaluated on every call. This second point causes a well-known gotcha covered later in this lesson.
Delegation is especially useful for the decorator pattern: wrapping an existing implementation to add logging, caching, validation, or metrics, while overriding only the one or two methods you actually want to change and letting the rest of the interface pass straight through automatically. This keeps decorators small and focused, and it avoids the fragile-base-class problems that come from deep inheritance hierarchies.
Kotlin also has a separate but similarly-named feature, property delegation (by lazy, by Delegates.observable), which uses the same by keyword but a completely different mechanism (the getValue/setValue operator convention) to delegate how a single property is read or written. That is a distinct topic covered in its own lesson — this lesson focuses on delegating an entire class’s interface implementation.
Syntax
interface Base {
fun method()
}
class Derived(b: Base) : Base by b {
// optional: override specific members here
}
- Base — the interface being implemented. Delegation only works for interface types.
- b — the delegate object, usually passed in through the primary constructor. It must implement
Base. - by b — tells the compiler to forward every member of
BasethatDeriveddoesn’t override tob. - Derived — can override any subset of
Base‘s members; overridden members replace the forwarded ones, while the rest continue to be forwarded automatically.
Examples
Example 1: Basic delegation
interface SoundMaker {
fun makeSound(): String
}
class Dog : SoundMaker {
override fun makeSound() = "Woof!"
}
class RobotDog(soundMaker: SoundMaker) : SoundMaker by soundMaker
fun main() {
val robot = RobotDog(Dog())
println(robot.makeSound())
}
Output:
Woof!
RobotDog never implements makeSound() itself. It simply declares SoundMaker by soundMaker, so the compiler generates a makeSound() method on RobotDog that calls soundMaker.makeSound(). From the caller’s perspective, RobotDog is a fully valid SoundMaker even though it contains zero lines of sound-making logic.
Example 2: Decorator that overrides one method
interface MessagePrinter {
fun printMessage(message: String)
fun printError(message: String)
}
class ConsolePrinter : MessagePrinter {
override fun printMessage(message: String) {
println("MESSAGE: $message")
}
override fun printError(message: String) {
println("ERROR: $message")
}
}
class LoggingPrinter(printer: MessagePrinter) : MessagePrinter by printer {
override fun printError(message: String) {
println("[LOG] An error was reported")
println("ERROR: $message")
}
}
fun main() {
val printer: MessagePrinter = LoggingPrinter(ConsolePrinter())
printer.printMessage("System started")
printer.printError("Disk not found")
}
Output:
MESSAGE: System started
[LOG] An error was reported
ERROR: Disk not found
This is the decorator pattern in action. LoggingPrinter wraps a ConsolePrinter and only overrides printError to add a log line first. The call to printMessage is never written inside LoggingPrinter at all — it is forwarded automatically to the wrapped ConsolePrinter, because that member was not overridden.
Example 3: Delegating multiple interfaces
interface Flyer {
fun fly(): String
}
interface Swimmer {
fun swim(): String
}
class Duck : Flyer, Swimmer {
override fun fly() = "Duck flies low"
override fun swim() = "Duck paddles"
}
class SuperVehicle(flyer: Flyer, swimmer: Swimmer) : Flyer by flyer, Swimmer by swimmer
fun main() {
val duck = Duck()
val vehicle = SuperVehicle(duck, duck)
println(vehicle.fly())
println(vehicle.swim())
}
Output:
Duck flies low
Duck paddles
A class can delegate more than one interface at once, each to a possibly different object. Here SuperVehicle implements both Flyer and Swimmer purely by forwarding to the same duck instance twice, but you could just as easily pass two unrelated objects, letting SuperVehicle compose behavior from completely independent sources without any of them knowing about each other.
How It Works Step by Step
Walking through class Derived(b: Base) : Base by b:
- 1. At construction, the expression
bis evaluated exactly once and stored in a private, compiler-generated field insideDerived. - 2. For every abstract member declared in
Base, the compiler checks whetherDerivedsupplies its ownoverride. If not, it synthesizes a matching method onDerivedwhose body simply calls that same member on the stored field and returns the result. - 3. If
Deriveddoes override a member, that hand-written implementation is used instead of the generated forwarding method for calls made on a Derived reference. - 4. Crucially, this dispatch is not virtual across the delegate boundary: if the delegate object’s own code calls one of its other methods internally (for example, method A calling method B on
this), that call resolves against the delegate object itself — it has no idea it has been wrapped, so it never reachesDerived‘s override of B. This is fundamentally different from inheritance, where an overridden method is picked up by virtual dispatch even when called from inside the base class. - 5. Multiple
byclauses (one per delegated interface) can be combined in a single class header, each with its own delegate expression.
Common Mistakes
Mistake 1: Expecting a reassigned var to change the delegate
It is tempting to declare the delegate as a mutable constructor property and swap it later, assuming the delegation will pick up the new value. It won’t — the delegate was captured once, at construction, into a separate hidden field.
interface Greeter {
fun greet(): String
}
class EnglishGreeter : Greeter {
override fun greet() = "Hello!"
}
class SpanishGreeter : Greeter {
override fun greet() = "¡Hola!"
}
class GreeterHolder(var delegate: Greeter) : Greeter by delegate
fun main() {
val holder = GreeterHolder(EnglishGreeter())
println(holder.greet())
holder.delegate = SpanishGreeter()
println(holder.greet())
}
Output:
Hello!
Hello!
Even though holder.delegate was reassigned to a SpanishGreeter, greet() still prints Hello! the second time, because the by delegate clause forwards to the object captured when GreeterHolder was constructed, not to whatever the delegate property currently holds. If you need the delegation target to change at runtime, implement the interface yourself and forward through the property explicitly:
interface Greeter {
fun greet(): String
}
class EnglishGreeter : Greeter {
override fun greet() = "Hello!"
}
class SpanishGreeter : Greeter {
override fun greet() = "¡Hola!"
}
class GreeterHolderFixed(var delegate: Greeter) : Greeter {
override fun greet() = delegate.greet()
}
fun main() {
val holder = GreeterHolderFixed(EnglishGreeter())
println(holder.greet())
holder.delegate = SpanishGreeter()
println(holder.greet())
}
Output:
Hello!
¡Hola!
Mistake 2: Assuming an override affects the delegate’s internal calls
A subtler mistake: overriding one method of a delegated interface and assuming other delegated methods that internally call it will now use your override. They won’t, because the delegate object’s own methods call each other on itself, not on the wrapper.
interface Calculator {
fun square(x: Int): Int
fun sumOfSquares(a: Int, b: Int): Int
}
class BasicCalculator : Calculator {
override fun square(x: Int) = x * x
override fun sumOfSquares(a: Int, b: Int) = square(a) + square(b)
}
class LoggingCalculator(calc: Calculator) : Calculator by calc {
override fun square(x: Int): Int {
println("squaring $x")
return x * x
}
}
fun main() {
val calc = LoggingCalculator(BasicCalculator())
println(calc.sumOfSquares(2, 3))
}
Output:
13
No "squaring" lines are printed. sumOfSquares was not overridden, so the call forwards straight to BasicCalculator.sumOfSquares, which calls square(a) + square(b) on itself — not on LoggingCalculator. The fix is to override every method whose behavior needs to reflect the change, not just the one you touched:
interface Calculator {
fun square(x: Int): Int
fun sumOfSquares(a: Int, b: Int): Int
}
class BasicCalculator : Calculator {
override fun square(x: Int) = x * x
override fun sumOfSquares(a: Int, b: Int) = square(a) + square(b)
}
class LoggingCalculatorFixed(private val calc: Calculator) : Calculator by calc {
override fun square(x: Int): Int {
println("squaring $x")
return x * x
}
override fun sumOfSquares(a: Int, b: Int) = square(a) + square(b)
}
fun main() {
val calc = LoggingCalculatorFixed(BasicCalculator())
println(calc.sumOfSquares(2, 3))
}
Output:
squaring 2
squaring 3
13
Best Practices
- Reach for class delegation when you want to reuse an existing interface implementation with small, targeted variations — it is a lightweight alternative to subclassing an
openbase class. - Override only the members you actually need to change; let everything else forward automatically so decorators stay small and easy to read.
- Remember delegation is composition, not inheritance: calls made from inside the delegate object never dispatch back into your overrides. If several interface members must stay consistent with each other, override all of them together.
- Prefer delegating to a
valreference captured at construction. If you genuinely need to swap the underlying implementation later, implement the interface by hand and forward manually through avarproperty instead of relying onby. - Keep the delegate parameter
privatewhen the class doesn’t need to expose it, so callers only see the interface, not the wrapping detail. - Use delegation to build focused decorators for cross-cutting concerns — logging, caching, retry logic, metrics — instead of scattering that logic across every implementation of an interface.
Practice Exercises
- Define an interface
Storagewithfun save(key: String, value: String)andfun load(key: String): String?. Write anInMemoryStorageimplementation backed by aMutableMap. Then write aLoggingStorageclass that delegates to aStorageinstance and overridesloadto print"loading $key"before returning the result. - Given an interface
Shapewithfun area(): Doubleandfun perimeter(): Double, and aCircleimplementation, write aLoggingShapedecorator via delegation that prints a message every timearea()is called, whileperimeter()passes through untouched. Calling both methods should print a log line only before the area result. - Predict, then verify by writing the code, what happens if a delegated interface has three methods, two are left un-overridden, and the third (overridden) method is never called by the other two internally — confirm the override still works normally for direct external calls even though the internal-call gotcha from Mistake 2 doesn’t apply here.
Summary
- The
byclause on a class header (class Derived(b: Base) : Base by b) implementsBaseby forwarding its members tob, generated automatically by the compiler. - Delegation only works against interfaces, since the compiler needs a fixed member list to know what to forward.
- The delegate expression is evaluated once, at construction, and cached in a hidden field — reassigning a
varproperty used as the delegate does not change what is delegated to. - You can override any subset of the delegated interface’s members; unoverridden members keep forwarding automatically, which is ideal for decorators.
- Delegation is composition, not inheritance: a delegate’s internal self-calls are not virtually dispatched to your overrides, unlike calls inside an
openbase class. - A single class can delegate multiple interfaces at once, each to the same or a different object.
