Kotlin Interface Delegation with by

Forward an interface to a composed object and understand overrides, delegate identity and explicit dependency ownership.

Lesson 30 of 43Advanced9 min read

Sometimes a class needs to offer an interface while another object already knows how to perform that interface's work. You could write a forwarding method for every operation, or Kotlin can generate those forwards with by. This is interface delegation: reuse through a contained object rather than inheriting its implementation class.

The inheritance and interfaces lesson gives the needed contract vocabulary. This reference explains how delegation interacts with overrides and why the delegate's internal calls still belong to the delegate. You will build a small formatted-message wrapper without pretending delegation automatically intercepts every call made inside another object.

The interface remains the public promise

An interface states what callers can do. A delegate is the particular object doing some or all of that work. In a declaration such as class Wrapped(base: Formatter) : Formatter by base, the wrapper implements Formatter and stores the supplied object for generated forwarding.

The caller can use Wrapped through the Formatter contract without depending on the delegate's concrete class. That supports replacement in tests or configuration. A deterministic formatter can stand in for an expensive implementation while the wrapper's own policy remains testable.

Delegation does not grant more capabilities than the interface defines. If the concrete delegate has another method, it does not automatically become a method of the wrapper. This is useful: the wrapper can expose a focused contract instead of inheriting every public operation of a broad implementation class.

Selective overrides belong to the wrapper

The wrapper can override an interface member to replace the generated forward for calls made through the wrapper. Other members continue forwarding. That lets a wrapper add a narrow rule, such as validating text before delegating formatting or providing a different public label.

Store the delegate in a named property when your override must call it explicitly. Calling the wrapper's own method again can recurse instead of reaching the delegate. A private val base makes ownership and forwarding intent visible without giving callers direct access to bypass the wrapper's policy.

Do not assume generated forwarding wraps every internal action. If the delegate's method calls another method on itself, that call uses the delegate's own implementation. It does not route back through the wrapper's override. This is the most important behavioral difference to understand before using delegation for security-sensitive interception.

An override is not universal interception

Imagine an interface with a label property and a render method. A wrapper overrides label, but render is delegated. If the delegate's render reads its own label, it still sees the delegate's label. Reading wrapper.label directly sees the override. Both outcomes follow the actual object executing the code.

That means a wrapper cannot safely enforce authorization by overriding one helper while assuming every delegated operation consults that helper through the wrapper. Enforce the policy at each exposed operation or use an implementation explicitly designed to receive the policy dependency.

Write tests for the specific call path you intend to control. Test a direct wrapper member call and a delegated operation that uses related state internally. The small difference can matter when a decorator adds logging, validation, caching or permission checks. Clear composition should not rely on an imagined self-call interception mechanism.

Delegate lifetime and state ownership

The delegate object supplied during construction is the forwarding target. Changing an unrelated variable that originally referred to it does not automatically replace the wrapper's stored target. If runtime replacement is required, implement an explicit strategy or provider with a documented lifetime.

Several wrappers can share one mutable delegate. That may be intentional, but state changes can then affect more than one caller. Delegation does not deep-copy the object or isolate its cache. Decide whether the delegate is immutable, per operation, per user or shared across the application.

The official delegation guide documents forwarding and the internal-call limit. These are established language features. They differ from delegated properties, where by controls property access through getValue and setValue. The shared keyword describes two related mechanisms with distinct contracts.

Kotlin Interface Delegation with by Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Forward a formatter contract

LabelFormatter contains no manual format implementation because the compiler supplies the forwarding method. The interface is explicit at both the constructor and call site. If the wrapper has no additional responsibility, using the delegate directly may be enough; avoid adding a class solely because the language can generate it.

Kotlin
interface Formatter {
    fun format(text: String): String
}

class Brackets : Formatter {
    override fun format(text: String): String = "[$text]"
}

class LabelFormatter(base: Formatter) : Formatter by base

fun main() {
    val formatter: Formatter = LabelFormatter(Brackets())
    println(formatter.format("Pen"))
}
Output
[Pen]

Kotlin Interface Delegation with by in Real Projects

Worked exercise: validate before formatting

Create a formatter wrapper that rejects a blank input before calling the underlying formatter. Expose only the Formatter interface. The successful example should trim the input and format it once. Keep the original delegate private so callers cannot bypass validation through the wrapper's public API.

Kotlin
interface Formatter {
    fun format(text: String): String
}

class Brackets : Formatter {
    override fun format(text: String): String = "[$text]"
}

class CheckedFormatter(private val base: Formatter) : Formatter by base {
    override fun format(text: String): String {
        val cleaned = text.trim()
        require(cleaned.isNotEmpty()) { "Label cannot be blank" }
        return base.format(cleaned)
    }
}

fun main() {
    val formatter = CheckedFormatter(Brackets())
    println(formatter.format(" Pen "))
}
Output
[Pen]

The override has a real responsibility and calls the private delegate deliberately. Test blank input as a separate rejection case. If the interface later gains another formatting operation, review whether that operation also needs validation; default forwarding will not automatically copy this method's policy onto unrelated members.

Kotlin Interface Delegation with by FAQ

Is delegation the same as inheritance?

No. The wrapper implements a contract while containing another object that supplies behavior. It does not become a subclass of the delegate's implementation class. This can avoid inheriting unrelated methods and allow an implementation to be supplied at construction. Both approaches still need compatible contracts. Choose delegation when the relationship is uses or forwards to, and inheritance when a meaningful subtype relationship and shared implementation workflow justify it.

Will the delegate call my wrapper override internally?

Not merely because the wrapper delegates that interface. A method executing on the delegate uses the delegate's own members and state. The wrapper's override affects calls made through the wrapper. This distinction matters for authorization, caching and logging. If all internal work must use a policy, supply that policy to the implementation explicitly or design a shared workflow that invokes intended extension points. Do not infer interception from a concise by declaration.

Can two wrappers share the same delegate?

Yes. They then share the delegate's object identity and any mutable state it owns. That can be useful for one application-wide service or risky for user-specific caches. Delegation does not clone or partition the target. Document the lifetime and concurrency contract, and supply separate delegate instances when isolation is required. A constructor parameter makes the ownership choice visible, but the application still needs to choose the correct instance scope.

When is manual forwarding better?

Manual forwarding can be clearer when only a few methods exist, when every operation needs a visible policy or when target selection varies per call. Generated delegation saves repetitive code, but it can hide the fact that a newly added interface method receives default forwarding without review. Use a design that makes important checks explicit. A small amount of purposeful forwarding is preferable to a concise declaration that obscures authorization or validation responsibilities.

Kotlin Interface Delegation with by Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

A wrapper must validate one operation before forwarding. Which implementation fits?

Kotlin Interface Delegation with by Quiz

2 questions to check what you learned.

  1. What does Interface by base generate?

    • Interface-member forwarding to the supplied object
    • A deep clone of base
    • A new network connection automatically
  2. A delegated method reads its own object's property. Which property does it normally see?

    • Always the wrapper override
    • A random global property
    • The delegate's own property implementation