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:
java.util.Collections.synchronizedList(list):SynchronizedListimplementsList<E>, wraps an underlyingList<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 } }java.io.FilterInputStreamand Decorators:BufferedInputStreamandGZIPInputStreamextendFilterInputStream, which holdsprotected volatile InputStream inand forwards allread(),skip(), andclose()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:
- 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 (
invokevirtualorinvokeinterface) 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.
- The virtual call (
- Bimorphic Inline Caching:
When two delegate implementations are swapped at runtime (e.g.,
PrimaryDatabaseDelegateandFallbackDatabaseDelegate), 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: - 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 (
bykeyword):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
@Delegatein 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:
- 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.
- 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:
- The Decorator implements the exact same interface as the wrapped object (
Decorator implements Component). - The Decorator retains a private reference to a
Componentdelegate. - For standard operations, the decorator forwards the call to the delegate.
- 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 inCollections.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:
- Interface Segregation Principle (ISP) Violation & API Leakage: If you annotate a broad interface like
java.util.Listorjava.sql.Connection, Lombok blindly exposes all public methods to external callers—including mutating operations (clear(),remove()) that your class may intend to forbid. - 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
DelegatingFilterProxydelegates 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).
Related Topics
- What is composition and inheritance — Fundamental composition mechanics
- When is it better to use composition instead of inheritance — Architectural decision criteria
- What is Open Closed Principle — Strategy and decorator extensibility
- How is Single Responsibility related to cohesion — High cohesion through delegation