📐 Section 18 · Question #12

What is delegation in OOP

Consider a Project Manager and a Software Engineer:


🟢 Junior Level

Delegation is an Object-Oriented design technique where an object (the Delegator) receives a request from a client and hands off (delegates) the actual execution to a dedicated helper object (the Delegate). To external consumers, the delegator remains the primary entry point and contract holder, even though internal mechanics are executed by the delegate.

Client ──► Delegator (Exposes Public API) ──► Delegate (Executes Heavy Lifting)

Real-World Analogy

Consider a Project Manager and a Software Engineer:

  • A client requests: “Add Apple Pay integration to the checkout flow.”
  • The Project Manager does not write the backend code personally. Instead, the manager delegates the technical implementation to the Software Engineer.
  • To the client, the Project Manager remains the single responsible interface who accepts requirements and reports deliverables.

Java Code Example

// 1. The Delegate (Performs the actual task)
public class SmsSender {
    public void send(String phoneNumber, String message) {
        System.out.println("Dispatching SMS to " + phoneNumber + ": " + message);
    }
}

// 2. The Delegator (Exposes public API and forwards work to delegate)
public class NotificationService {
    // Composed delegate instance
    private final SmsSender smsSender;

    public NotificationService(SmsSender smsSender) {
        this.smsSender = smsSender;
    }

    public void notifyCustomer(String phone, String text) {
        // Pre-processing, audit logging, or validation
        System.out.println("Validating phone number and logging request...");

        // DELEGATION: hand off execution to helper
        smsSender.send(phone, text);
    }
}

Why Delegation Matters:

  • Clean Alternative to Inheritance: Avoids rigid class hierarchies and fragile base classes by composing objects dynamically.
  • Runtime Flexibility: The delegate instance can be swapped dynamically via constructor injection, setter injection, or strategy configuration.
  • Single Responsibility Principle (SRP): Keeps the delegator focused on coordination, delegation, and orchestration while the delegate focuses strictly on implementation details.

🟡 Middle Level

Forwarding vs. True Delegation (The Self-Problem)

In academic Object-Oriented theory (Henry Lieberman, 1986; Gang of Four), a critical distinction exists between Method Forwarding and True Delegation:

Evaluation Criterion Method Forwarding (Common Java Idiom) True Delegation (Theoretical OOP)
Context Passing (this) No. Object A simply invokes b.operation(). Inside operation(), this references object B. Yes. Object A passes its own reference: b.operation(this). Inside B, the execution context remains A.
Java Prevalence 99% of idiomatic Java code (Composition, Decorators, Wrappers). Visitor, State, or Strategy patterns where the owner explicitly passes this to the helper.
The Self-Problem Absent. Methods in B are completely isolated from A. Present. If B calls self-methods (this.doX()), it can unintentionally bypass polymorphic overrides intended by A.

Delegation in the Java Standard Library (JDK)

Delegation is pervasive across the JDK:

  1. java.util.Collections.synchronizedList(list): SynchronizedList implements List<E>, wraps an underlying List<E> delegate, and synchronizes access on an internal mutex:
    static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
        final List<E> list; // The Delegate
    
        SynchronizedList(List<E> list) {
            super(list);
            this.list = list;
        }
    
        public E get(int index) {
            synchronized (mutex) { return list.get(index); } // Delegated call inside lock
        }
    
        public E set(int index, E element) {
            synchronized (mutex) { return list.set(index, element); } // Delegated call inside lock
        }
    }
    
  2. java.io.FilterInputStream and Decorators: BufferedInputStream and GZIPInputStream extend FilterInputStream, which holds protected volatile InputStream in and forwards all read(), skip(), and close() calls to that wrapped stream delegate.

Compile-Time Delegation vs. Dynamic Proxies

Compile-Time Delegation: Direct Bytecode Invocation ──► JIT Inlining (0-1 ns)
Dynamic Proxy (JDK/CGLIB): Method.invoke() / Interceptor Stack ──► Reflection Overhead (30-150 ns)
Dimension Explicit Delegation (Compile-Time) Dynamic Proxy (java.lang.reflect.Proxy / ByteBuddy)
Mechanism Explicit code / IDE forwarding / Lombok @Delegate. Runtime bytecode generation (InvocationHandler).
Type Safety Verified at compile time by javac. Latent runtime errors if signatures or interfaces mismatch.
Latency 0.3–1 ns (reducible to 0 ns with JIT inlining). 30–150 ns per call (virtual method table indirection, interceptor chains).
Primary Use Case Domain logic composition, GoF structural patterns. Cross-cutting infrastructure concerns (Spring AOP, @Transactional, Security).

🔴 Senior Level

The Self-Problem in Simulated Delegation

In prototype-based or native delegation languages (Self, JavaScript prototype chains), dispatching an operation on an object automatically retains the original receiver as this. In class-based Java, simulating true delegation requires explicit self-passing:

public class Window {
    private final Rectangle bounds;

    public Window(Rectangle bounds) {
        this.bounds = bounds;
    }

    public int getArea() {
        // Must explicitly pass 'this' to let delegate query delegator's context
        return bounds.getArea(this);
    }

    public int getBorderThickness() {
        return 4; // Window has specific borders
    }
}

public class Rectangle {
    private final int width;
    private final int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public int getArea(Window context) {
        // Delegate queries delegator context to calculate final bounded area
        int border = context.getBorderThickness();
        return (width + border * 2) * (height + border * 2);
    }
}

If self-passing is omitted, the delegate operates exclusively on its local state, breaking late-bound polymorphic coordination between delegator and delegate.


HotSpot C2 JIT Devirtualization and Inlining Analysis

A common interview critique argues: “Delegation introduces extra pointer chasing and call stack frames compared to inheritance.”

Modern HotSpot C2 compilers disprove this assertion:

  1. Monomorphic Call Site Devirtualization: When a delegate field is declared private final Delegate delegate, HotSpot’s Class Hierarchy Analysis (CHA) verifies whether only one concrete implementation of the delegate exists in loaded class space. If monomorphic:
    • The virtual call (invokevirtual or invokeinterface) is devirtualized into a direct call.
    • HotSpot C2 performs Method Inlining, injecting the delegate’s raw bytecode instructions directly into the caller frame.
    • The method invocation overhead collapses to 0 nanoseconds.
  2. Bimorphic Inline Caching: When two delegate implementations are swapped at runtime (e.g., PrimaryDatabaseDelegate and FallbackDatabaseDelegate), the C2 compiler generates an inline cache with a direct pointer comparison:
    cmp   klass, [rax + 0x08]    ; Compare object header Klass pointer
    jne   fallback_label         ; Branch to second receiver if mismatched
    ; Inlined Body of PrimaryDatabaseDelegate
    jmp   exit_label
    fallback_label:
    ; Inlined Body of FallbackDatabaseDelegate
    exit_label:
    
  3. Megamorphic Fallback: When three or more implementations encounter the call site, the compiler downgrades to an itable (Interface Table) lookup, which introduces ~10–15 CPU cycles of indirect branching overhead.

Modern Language Mechanics: Kotlin by vs. Lombok @Delegate

Delegation boilerplate (forwarding 20 interface methods manually) led modern languages to introduce compiler-level automation:

  • Kotlin Native Class Delegation (by keyword):
    interface Repository<T> {
        fun findById(id: Long): T?
        fun save(entity: T): T
    }
    
    class CachedRepository<T>(
        private val target: Repository<T>,
        private val cache: Cache<Long, T>
    ) : Repository<T> by target {
        // Only override methods needing interception; compiler generates the rest!
        override fun findById(id: Long): T? {
            return cache.get(id) ?: target.findById(id)?.also { cache.put(id, it) }
        }
    }
    
  • Lombok @Delegate in Java:
    public class CustomCacheMap<K, V> implements Map<K, V> {
        @Delegate
        private final Map<K, V> underlying = new ConcurrentHashMap<>();
        // Lombok bytecode generation writes all 25+ Map forwarding methods during javac compilation.
    }
    

4 Tricky Questions

1. What happens when circular delegation occurs, and how do you prevent it?

Answer: If Object A delegates a task to Object B, and during execution Object B calls back into Object A’s delegated method, an infinite recursive cycle is initiated. This rapidly exhausts JVM thread call stack space, triggering a fatal java.lang.StackOverflowError.

Mitigation Strategies:

  1. Architectural DAG Rule: Dependencies and delegation must strictly conform to a Directed Acyclic Graph (DAG). Layer $N$ may delegate to Layer $N-1$, never in reverse.
  2. Automated CI Architecture Enforcement: Integrate ArchUnit rules in continuous integration:
    @ArchTest
    public static final ArchRule no_circular_dependencies =
        slices().matching("com.company.service.(*)..")
                .should().beFreeOfCycles();
    

2. Is the Decorator pattern a specialized form of delegation?

Answer: Yes. Decorator is an applied specialization of delegation combined with unified interface polymorphism:

  1. The Decorator implements the exact same interface as the wrapped object (Decorator implements Component).
  2. The Decorator retains a private reference to a Component delegate.
  3. For standard operations, the decorator forwards the call to the delegate.
  4. For decorated operations, it intercepts the call, executing additional pre- or post-processing behavior (e.g., encryption, caching, buffering) before or after delegating.

3. How does delegation affect thread safety when both delegator and delegate maintain mutable state?

Answer: Synchronizing methods on the delegator (synchronized(this)) does not protect the internal state of the delegate if:

  • Other components hold a direct reference to that same delegate object.
  • The delegate spawns internal asynchronous tasks or accesses shared static state.

To ensure thread safety in delegation:

  • Complete Encapsulation: Never expose internal delegates via public getters. Practice defensive copying or initialize delegates strictly within the delegator’s constructor.
  • Coordinate Mutexes: If the delegator synchronizes on mutex, the delegate must share that exact same lock object (as seen in Collections.synchronizedList(list, mutex)).
  • Prefer Stateless or Thread-Safe Delegates: Keep delegates stateless or back them with thread-safe primitives (AtomicReference, ConcurrentHashMap).

4. What are the severe risks of using Lombok @Delegate in enterprise applications?

Answer: While Lombok @Delegate removes boilerplate, it introduces two major architectural liabilities:

  1. Interface Segregation Principle (ISP) Violation & API Leakage: If you annotate a broad interface like java.util.List or java.sql.Connection, Lombok blindly exposes all public methods to external callers—including mutating operations (clear(), remove()) that your class may intend to forbid.
  2. Fragile Upstream Compilations: If a dependency updates and adds new methods to the delegate class or interface, Lombok will generate forwarding methods during your next build. If those new methods conflict in signature or return type with methods already declared on your delegator, compilation breaks unexpectedly.

Best Practice: Generate forwarding methods explicitly using IDE tools (IntelliJ IDEA -> Generate -> Delegate Methods), exposing only the precise methods required by your bounded context.


🎯 Interview Cheat Sheet

30-Second Summary

  • Definition: An architectural pattern where an object handles incoming requests by passing execution to an internal helper object rather than relying on class inheritance.
  • Relationship to Composition: Delegation is the execution mechanism of composition (“favor composition over inheritance”).
  • Forwarding vs. True Delegation: Java primarily uses method forwarding. True delegation requires passing the caller’s context (this) to the helper.
  • JVM Optimization: The HotSpot C2 compiler devirtualizes and inlines monomorphic delegates (private final Delegate delegate) via CHA, resulting in zero nanosecond overhead.

Common Follow-up Questions

  • “How does Delegation compare to Inheritance for code reuse?” → Inheritance forces compile-time static binding and exposes base class internals. Delegation provides runtime swappability, looser coupling, and strict encapsulation.
  • “How does Spring use delegation under the hood?” → Spring Security’s DelegatingFilterProxy delegates standard servlet filtering to a Spring-managed bean filter. Spring AOP uses CGLIB/JDK dynamic proxies to delegate cross-cutting concerns to target advice chains.

Red Flags (DO NOT Say)

  • ❌ “Delegation always causes a severe performance penalty due to extra method hops.” (JIT C2 monomorphic inlining eliminates call overhead completely).
  • ❌ “Delegation and inheritance are interchangeable.” (Inheritance creates an IS-A relationship with fragile base coupling; delegation establishes a HAS-A relationship with independent encapsulation).
  • ❌ “Lombok @Delegate should be used on all wrappers to avoid typing.” (It blindly leaks mutating API methods, violating ISP and encapsulation).