What is Delegation in OOP
@Transactional creates a proxy that delegates calls to the real bean, wrapping them in a transaction. Overhead ~50-200ns per call, but provides declarative transactionality.
🟢 Junior Level
Delegation is a mechanism where an object does not perform a task itself, but entrusts it to another object while retaining responsibility for the result. It’s like when a manager assigns a task to a developer but remains accountable to the client.
Simple analogy: You want to order a pizza. Instead of cooking it yourself (inheritance), you call a pizzeria (delegation). You are the interface, the pizzeria is the implementation.
Delegation example:
// Delegate — does the work
public class Printer {
public void print(String text) {
System.out.println("Printing: " + text);
}
}
// Delegator — assigns the work
public class DocumentProcessor {
private final Printer printer; // Delegate
public DocumentProcessor(Printer printer) {
this.printer = printer;
}
public void processAndPrint(String doc) {
// document processing
printer.print(doc); // Delegation of printing
}
}
When to use:
- When you need to reuse code without inheritance
- When you need the ability to change implementation at runtime
- For implementing Strategy, State, Decorator patterns
🟡 Middle Level
How It Works
Delegation = an object (Delegator) forwards calls to another object (Delegate). In Java this is most commonly Method Forwarding — an explicit call to the delegate’s method.
Difference from a simple call: In “pure” delegation, the delegate may hold a reference to the delegator’s context (passing this). In Java, simple forwarding is more common.
Practical Application
Example from JDK — Collections.synchronizedList:
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// SynchronizedList delegates to ArrayList, adding synchronized
Kotlin: built-in delegation:
class MyList(inner: List<String>) : List<String> by inner
// The compiler generates all delegate methods automatically
Delegation vs Proxy
| Characteristic | Delegation | Proxy |
|---|---|---|
| Binding | Static (code) | Dynamic (runtime) |
| Implementation | Explicit methods | java.lang.reflect.Proxy / CGLIB (library for generating bytecode proxy subclasses) |
| Performance | Faster (direct method call — JVM may inline, ~0.3ns) | Slower (reflection via java.lang.reflect.Proxy, ~100-500ns) |
| Flexibility | Fixed | Can be changed on the fly |
Common Mistakes
| Mistake | Solution |
|---|---|
| Circular delegation (A→B→A) → StackOverflow | Architectural analysis |
| Delegate without access to private fields | Pass context through parameters |
| Manually writing 20 delegate methods | Use IDE Generate / Lombok |
When NOT to Use Delegation
- Simple utilities (static methods are better)
- When boilerplate overhead exceeds the benefit
- Performance-critical sections (low latency)
🔴 Senior Level
Internal Implementation at the JVM Level
Method Forwarding — bytecode:
INVOKEVIRTUAL Printer.print(Ljava/lang/String;)V
→ invokevirtual → VTable lookup → Printer.print()
With final field:
private final Printer printer;
→ monomorphic call → JIT inlines → direct call (0.3ns)
JIT Optimization: If the delegate field is final, the JIT compiler can inline the delegate’s code (embed the method body directly at the call site, removing call overhead entirely). This is called monomorphic inline caching — the JVM sees one type and optimizes.
Architectural Trade-offs
Manual Delegation:
- ✅ Pros: Compile-time safety, no reflection overhead, IDE support
- ❌ Cons: Boilerplate, maintenance burden
Dynamic Proxy:
- ✅ Pros: Zero boilerplate, runtime flexibility
- ❌ Cons: Reflection overhead (~10-100x slower), no compile-time checks
Edge Cases
- Circular Delegation: A→B→A → StackOverflowError
- Solution: Architectural tests (ArchUnit), explicit prohibition
- Visibility Limitation: Delegate cannot see delegator’s private fields
- Solution: Pass context through parameters, package-private access
- Long Delegation Chains: A→B→C→D → hard to debug
- Solution: Limit to 2-3 hops, monitoring via stack traces
Performance
| Approach | Latency | Note |
|---|---|---|
| Direct call | ~0.3 ns | Baseline |
| Final delegate | ~0.3 ns | JIT inlined |
| Non-final delegate | ~1-3 ns | Monomorphic |
| JDK Proxy | ~100-500 ns | Reflection |
| CGLIB Proxy | ~50-200 ns | Bytecode gen |
- Inlining: final → 0 overhead
- Memory: +16 bytes per delegate object header
Thread Safety
- Stateless delegates: Thread-safe by default
- Stateful delegates: Need synchronization
- Concurrent delegation: Can use different delegate instances per thread
class ConcurrentProcessor {
private final ThreadLocal<Delegate> delegate =
ThreadLocal.withInitial(Delegate::new);
// One delegate per thread — no contention
}
Production Experience
Spring AOP (Aspect-Oriented Programming — allows adding cross-cutting concerns like transactions without modifying business code) via delegation:
@Transactional creates a proxy that delegates calls to the real bean, wrapping them in a transaction. Overhead ~50-200ns per call, but provides declarative transactionality.
Real case: Migration from JDBC to JPA — extracted DataAccessDelegate, switched delegate without changing business logic. Result: zero-downtime migration.
Monitoring
ArchUnit (library for architectural tests — checks dependencies between classes at the junit test level):
@ArchTest
static void no_circular_delegation = slices()
.matching("*(..)")
.should().notCycleDependency();
SonarQube (static code analyzer — finds code smells, bugs, vulnerabilities): Max delegation chain length → configure threshold
Best Practices for Highload
- final delegates for inlining
- Stateless delegates for thread-safety
- Max 2-3 hops for debuggability
- Generate delegates via IDE/Lombok for boilerplate
🎯 Interview Cheat Sheet
Must know:
- Delegation = object entrusts a task to another, retaining responsibility for the result
- Method Forwarding — explicit call to the delegate’s method, faster than Dynamic Proxy
finaldelegate → JIT inlines → 0 overhead (~0.3 ns), non-final → monomorphic (~1-3 ns)- JDK Proxy (~100-500 ns) vs CGLIB Proxy (~50-200 ns) vs manual delegation (~0.3 ns)
- Circular delegation (A→B→A) → StackOverflowError — needs architectural control
- Spring AOP
@Transactional— proxy delegates calls to the real bean, overhead ~50-200ns
Frequent follow-up questions:
- Delegation vs Proxy? — Delegation = static (compile-time), Proxy = dynamic (runtime via reflection)
- How to avoid Circular Delegation? — ArchUnit tests for dependency cycles, explicit prohibition
- Why is
finalimportant for a delegate? — JIT sees one type → monomorphic inline caching → full inlining - Delegation and Thread Safety? — Stateless delegates are thread-safe by default; ThreadLocal for stateful
Red flags (DO NOT say):
- “Delegation is always slower than a direct call” (with a
finalfield — same ~0.3 ns after inlining) - “Dynamic Proxy is always the best choice” (100-500ns overhead, critical in hot paths)
- “Long delegation chains are fine” (Max 2-3 hops, otherwise hard to debug)
Related topics:
- [[10. What is composition and inheritance]]
- [[11. When is it better to use composition instead of inheritance]]
- [[4. How to refactor code that violates Open Closed principle]]
- [[3. What is Open Closed principle]]