Question 12 · Section 18

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.

Language versions: English Russian Ukrainian

🟢 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

  1. Circular Delegation: A→B→A → StackOverflowError
    • Solution: Architectural tests (ArchUnit), explicit prohibition
  2. Visibility Limitation: Delegate cannot see delegator’s private fields
    • Solution: Pass context through parameters, package-private access
  3. 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
  • final delegate → 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 final important 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 final field — 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]]