skip to content

What are the design limitations and risks of interface delegation via `by` — especially around interface evolution and which members are NOT forwarded?

level: principalimportance: nice to knowfreq 18%

answer

  1. Forwards only the declared interface members
  2. Interfaces only — never classes
  3. New interface members get silent auto-forwarders
  4. Captured once at construction (immutable forwarding)
  5. Forwards accessors, not backing fields

basics

~20 s

by only forwards members declared by the interface. It can't delegate classes, won't auto-forward new collaborators, and the forwarding is fixed at construction. New interface methods get auto-forwarders too, which can silently expose unintended behavior.

solid answer

~40 s

Interface delegation forwards exactly the members of the delegated interface(s) — nothing else. It cannot delegate to a class (only interfaces are delegable). The delegate is captured once at construction, so forwarding is immutable thereafter. A subtle evolution risk: when the interface gains a new member, the compiler silently generates a forwarder for it, so your wrapper transparently exposes the new behavior — which may be wrong if your wrapper had invariants (e.g., a read-only wrapper that gains a mutating method). Conversely, delegated forwarders won't call your overrides via self-calls (no open recursion). Also, properties' backing state isn't shared — you forward accessors, not fields. For decorators that must guarantee invariants across all current and future members, explicit implementation (or sealing the interface) can be safer than blanket delegation.

code

kotlin · 9 lines
kotlin
// Invariant risk: a 'read-only' wrapper over an evolving interface.
interface Store {
    fun get(k: String): String?
    // Suppose a later version ADDS: fun put(k: String, v: String)
}

class ReadOnlyStore(impl: Store) : Store by impl
// If Store gains put(), ReadOnlyStore SILENTLY forwards it too -> invariant broken,
// with no compile error. Safer: implement Store explicitly and decide per-member.

go deeper

for a junior

Knows by works only for interfaces and forwards their members.

for a middle

Understands construction-time capture and that overrides only replace single forwarders.

for a senior

Articulates the no-open-recursion limit and accessor-not-field forwarding.

for a principal

Anticipates the silent-auto-forward evolution risk to wrapper invariants and chooses explicit implementation or narrow delegation accordingly.

## What `by` forwards — and what it doesn't Interface delegation generates forwarders for **exactly the members of the delegated interface type(s)** as seen at compile time. It does **not**: - Delegate **classes** — only interfaces are delegable (`by` requires an interface supertype). - Forward members the static type doesn't declare. If you delegate `List<T>` but the underlying object is a `MutableList<T>`, the wrapper exposes only `List` members; the mutators aren't reachable through the wrapper's `List` type. - Share **fields/backing state** — it forwards property *accessors* (getters/setters), not storage. ## Construction-time capture The `by` expression is evaluated **once** at construction and stored. Forwarders bind to that value; reassigning a `var` delegate does not redirect them. Delegation is therefore effectively immutable. ## Interface-evolution risk (the big one) Because the compiler auto-generates a forwarder for **every** interface member, when the interface **gains a new member**, your wrapper **silently** forwards it too. Example: you built `ReadOnlyView(list) : List<T> by list` assuming `List` is read-only. If a future version of the interface (or a broader interface you delegate) adds a state-mutating method, your wrapper would transparently expose it, breaking the read-only invariant — with **no compile error** to warn you. The forwarding is "blanket": it does not know your intended invariants. Mitigations: - Delegate the **narrowest** interface that exactly matches your invariant. - For invariant-critical wrappers, **implement explicitly** so new members force a compile error you must consider. - Consider sealing/limiting the delegated interface. ## No open recursion (self problem) Forwarders are one-way (outer → delegate). The delegate's internal self-calls never dispatch back to your overrides, so a wrapper cannot intercept the delegate's internal method calls. This is safer (no fragile-base-class self-call coupling) but limits interception. ## Override + delegate ordering An explicit override replaces the forwarder for that member only; everything else still forwards. To call through, the delegate must be a `val` property. ## When NOT to use `by` - When you need to intercept the delegate's *internal* calls. - When the interface is large and evolving and your wrapper has invariants over a subset. - When you actually need shared mutable state with the delegate. ## Keywords/mechanics involved `by`, `override`, interface-only delegation, construction-time capture, accessor (not field) forwarding, open recursion / self problem, fragile base class.

  • How do you protect a wrapper's invariant against future interface members?
    Implement the interface explicitly instead of using blanket `by`, or delegate only the narrowest interface; new members then force a compile-time decision.
  • Why can't you delegate a class with `by`?
    Delegation generates interface-member forwarders; classes can carry state and constructors, so Kotlin restricts `by` to interface supertypes only.
  • Does delegation share the delegate's backing fields?
    No. It forwards property getters/setters (accessors), not the underlying storage; there is no shared field.

Like signing a stand-in to do 'whatever the job description says' — if the job description later grows, your stand-in silently starts doing the new tasks too, even ones you'd have refused.

saying these in an interview costs you the question

  • Believing `by` warns you when the interface gains members
  • Claiming you can delegate to a class
  • Thinking delegation shares the delegate's backing fields
  • Assuming forwarders capture a live `var` reference that can be redirected
  • Treating blanket delegation as always safe for invariant-bearing wrappers

context