skip to content

Composition usually means delegation. What is delegation in practice, what boilerplate and pitfalls does it bring, and what is the "self-problem"?

level: seniorimportance: should knowfreq 40%

answer

  1. forward calls to a held collaborator
  2. Kotlin `by`, Go embedding = generated forwarders
  3. self-problem: inner this.foo() never reaches the wrapper
  4. mirror image of fragile base class
  5. wrapper breaks identity / instanceof

basics

~20 s

Delegation means an object forwards calls to a collaborator it holds. You get flexibility, but must write a forwarder per method, and the wrapped object's internal calls dispatch to itself — so your wrapper can't intercept them. That's the self-problem.

solid answer

~60 s

Delegation: an object implements an interface by forwarding some or all calls to a held collaborator, adding behavior before/after. It's the mechanic behind Decorator, Proxy, and Adapter, and it's how composition replaces overriding. Costs and pitfalls: - **Forwarding boilerplate** — one method per interface member. Language support helps: Kotlin's `class X(b: B) : B by b`, Go's struct embedding, C#'s explicit interface implementation plus source generators, Groovy `@Delegate`, dynamic proxies at runtime. - **The self-problem** — the wrapped object's internal `this.foo()` calls dispatch to *itself*, never up to the wrapper. So a logging or caching wrapper misses internally-triggered work. Inheritance has the opposite property (it intercepts everything, including calls the base makes internally — which is what makes the base fragile). - **Identity and equality** — a wrapper is not `==`/`equals` to the wrapped object; caches, identity maps, `instanceof` checks, serialization and reference equality all see a different object. - **Interface drift** — when the delegated interface gains a method, hand-written forwarders must be updated (auto-delegation silently forwards, which may be wrong). - **Stacked decorators** make stack traces deep and ordering semantically significant.

code

pseudocode · 6 lines
pseudocode
class CachingRepo(private val inner: Repo) : Repo by inner {   // forwarders generated
  override fun load(id) = cache.getOrPut(id) { inner.load(id) }
}

// self-problem: inner.loadAll() internally calls ITS OWN load(id),
// so those reads never touch CachingRepo.load and are not cached.

go deeper

for a junior

Define delegation as forwarding to a held object and note you must write one forwarder per method.

for a middle

Add that Decorator/Proxy/Adapter are built on it, mention language support for generated forwarders, and describe the self-problem with one example.

for a senior

Cover self-problem vs fragile base class as mirror images, identity/instanceof breakage, interface-evolution behavior of generated vs hand-written forwarders, and decorator ordering semantics.

for a principal

Discuss where cross-cutting concerns should live so the self-problem can't bite (boundary/facade interception, aspect or proxy layers), the observability cost of deep decorator stacks, and the policy of never subclassing types you don't own.

## What delegation is **Delegation** = object *A* receives a request and passes it to object *B* to fulfil, possibly adding behavior around it. *A* holds *B* (composition) rather than inheriting from it. It usually implements the same interface as *B*, so callers can't tell the difference. ``` interface Repo { fun load(id): Doc } class LoggingRepo(private val inner: Repo) : Repo { override fun load(id): Doc { log("load $id") return inner.load(id) // the delegation } } ``` This is the machinery behind: - **Decorator** — same interface, adds behavior, stackable (`Metrics(Caching(Logging(repo)))`). - **Proxy** — same interface, controls access (lazy load, remote call, permission check). - **Adapter** — *different* interface in, target interface out. - **Strategy** — the outer object delegates one decision to an injected collaborator. ## Cost 1 — boilerplate Wrapping a 30-method interface means 30 one-line forwarders that must stay in sync. Language and tooling answers: - **Kotlin**: `class LoggingRepo(private val inner: Repo) : Repo by inner { override fun load(id) = ... }` — the compiler generates all forwarders; you override only what you change. - **Go**: embedding a struct or interface promotes its methods automatically. - **Groovy**: `@Delegate`. **Lombok**: `@Delegate`. **C#**: source generators or `DispatchProxy`. - **Runtime dynamic proxies** (JDK `Proxy`, `InvocationHandler`; aspect frameworks) generate a wrapper at runtime — powerful, but you lose compile-time checking and stack traces get noisy. If the language gives you none of this, weigh the boilerplate honestly; for very wide interfaces, or when you truly need to intercept internal calls, inheritance may be pragmatic — with the fragility accepted knowingly. ## Cost 2 — the self-problem (the important one) With **inheritance**, `this` is one object. When base code calls `this.helper()`, dispatch finds the subclass override. Interception is total. With **delegation**, there are **two objects**. When the inner object calls its own `this.helper()`, `this` is the *inner* object — the wrapper is not in the picture and never sees the call. Concretely: you wrap a repository with caching. `repo.loadAll()` internally calls `this.load(id)` for each id. Your cache wraps the *outer* `load`, so those internal loads bypass the cache entirely. The behavior you thought you added is partial. This is the exact mirror image of the fragile base class problem: - Inheritance intercepts internal self-calls — sometimes *too much*, and the base can change them out from under you. - Composition intercepts nothing internal — sometimes *too little*, but you're immune to base refactors. Workarounds: design the inner type so cross-cutting concerns route through a delegate-aware seam (pass the outer object in, i.e. explicit "self" parameter — the technique sometimes called *consultation vs delegation*), split the inner type so no method calls its own siblings, or put the concern at a boundary (a facade/service layer) where all traffic must pass. ## Cost 3 — identity, equality, and type checks A wrapper is a distinct object: - `wrapper != inner` under reference equality; `equals` is whatever you write (forwarding `equals` breaks symmetry — `inner.equals(wrapper)` is usually false). - `instanceof ConcreteInner` / `is ConcreteInner` fails. Code that downcasts or switches on concrete types breaks when you introduce a wrapper — a good reason such code shouldn't exist. - Identity maps, caches keyed by object identity, `IdentityHashMap`, and serialization can see doubled or unexpected objects. - Frameworks that read annotations off the concrete class may not see them through a proxy. - Thread-safety and lifecycle: who closes the inner resource, the wrapper or the owner? Decide and document. ## Cost 4 — interface evolution When the delegated interface gains a method: - **hand-written** forwarders fail to compile → you're forced to decide (good); - **auto-generated** delegation (`by`, embedding) silently forwards the new method → your wrapper's cross-cutting concern silently doesn't apply to it (subtle). Neither is free; know which behavior your mechanism gives you. ## Cost 5 — debuggability Deep decorator stacks make traces long and make "which layer did this?" harder. Order matters semantically (retry-outside-cache behaves differently from cache-outside-retry) and is invisible at the call site — document the intended assembly in one place. ## When delegation clearly beats overriding - Adding behavior to a type you don't own (never subclass a third-party concrete class). - Behavior that should be composable and reorderable (metrics, retries, caching, auth). - Behavior that must change at runtime or be swapped in tests. - Wrapping to *narrow* an API (exposing 3 of 30 methods) — inheritance can only widen.

  • How does the self-problem compare to the fragile base class problem?
    They are mirror images. Inheritance makes the base's internal self-calls dispatch into your override — total interception, but the base can change those calls and break you. Delegation gives zero interception of internal calls — you're immune to base refactors, but your cross-cutting behavior can be silently incomplete.
  • Name a concrete bug caused by wrapper identity differing from the wrapped object.
    Code doing `if (repo instanceof SqlRepo) repo.rawQuery(...)` or a cache keyed by object identity stops matching once a logging decorator is inserted, silently taking a different branch or double-storing entries.
  • Does auto-generated delegation (Kotlin `by`, Go embedding) fully remove the boilerplate downside?
    It removes the typing, not the semantics. New interface methods are forwarded silently, so a wrapper meant to apply a policy to *every* call quietly stops covering the new one — hand-written forwarders would have failed to compile.

A receptionist forwarding calls: they can log and screen everything that comes through the switchboard, but they never hear the manager phoning a colleague on an internal line.

context