🧩 Section 2 · Question #14

What Proxy types exist

The Proxy pattern is a Structural design pattern that provides a surrogate, placeholder, or interceptor for another object to control, restrict, enrich, or delay access to it. T...


🟢 Junior Level

The Proxy pattern is a Structural design pattern that provides a surrogate, placeholder, or interceptor for another object to control, restrict, enrich, or delay access to it. The proxy implements the exact same interface as the target object, allowing it to perform pre-processing or post-processing operations before and after delegating the call to the real object.


Real-World Analogies

  • Debit Card: A debit card acts as a proxy for your bank account. Merchants interact with the card to verify funds, check balances, and hold transactions, but physical money resides in the bank vault.
  • Executive Assistant (Secretary): Callers do not walk straight into the CEO’s office. The assistant checks schedules, authenticates visitors, filters spam, and schedules access before delegating.

Functional Types of Proxies (GoF Classification)

  1. Virtual Proxy (Lazy Loading): Defers the instantiation of a heavy, memory-intensive object until the moment it is first invoked.
  2. Protection Proxy (Access Control & Security): Validates permissions, authentication, and authorization before forwarding the invocation to the real service.
  3. Remote Proxy (RPC / IPC): Represents an object located on a remote server, hiding network transport details (e.g., gRPC, Spring Cloud OpenFeign, RMI).
  4. Caching Proxy (Memoization): Caches the results of expensive operations and returns stored data for subsequent identical requests.
  5. Logging & Auditing Proxy: Intercepts invocations to log parameters, execution timings, and exceptions without modifying the underlying business class.

Code Example: Virtual Proxy (Lazy Loading)

// Common Interface
public interface DocumentViewer {
    void display();
}

// Heavy Target Object
public class HeavyReport implements DocumentViewer {
    private final String filename;

    public HeavyReport(String filename) {
        this.filename = filename;
        loadFromDisk(); // Expensive 3-second I/O parsing operation
    }

    private void loadFromDisk() {
        System.out.println("Loading large PDF file from disk: " + filename);
    }

    @Override
    public void display() {
        System.out.println("Rendering report: " + filename);
    }
}

// Virtual Proxy
public class LazyReportProxy implements DocumentViewer {
    private final String filename;
    private HeavyReport realReport; // Lazily allocated on first access

    public LazyReportProxy(String filename) {
        this.filename = filename;
    }

    @Override
    public void display() {
        if (realReport == null) {
            realReport = new HeavyReport(filename); // Instantiated only when requested!
        }
        realReport.display();
    }
}

🟡 Middle Level

Dynamic Proxies in Java: JDK Dynamic Proxy vs CGLIB / ByteBuddy

In production software, proxies are rarely coded manually for individual classes. Instead, Dynamic Proxies are synthesized at runtime.

1. JDK Dynamic Proxy (java.lang.reflect.Proxy)

A native Java Standard Library feature. Generates dynamic bytecode for a new class ($Proxy0) implementing specified interfaces:

  • Requirement: Requires an interface. Cannot proxy concrete classes directly.
  • Mechanism: Dispatches calls through an InvocationHandler.
public interface PaymentService {
    void pay(double amount);
}

public class PaymentServiceImpl implements PaymentService {
    public void pay(double amount) {
        System.out.println("Processing payment: $" + amount);
    }
}

// Dynamic JDK Proxy Creation:
PaymentService target = new PaymentServiceImpl();
PaymentService proxy = (PaymentService) Proxy.newProxyInstance(
    PaymentService.class.getClassLoader(),
    new Class<?>[]{PaymentService.class},
    (proxyObj, method, args) -> {
        System.out.println("[AUDIT] Invoking method: " + method.getName());
        long start = System.currentTimeMillis();
        Object result = method.invoke(target, args); // Delegation
        System.out.println("[AUDIT] Completed in " + (System.currentTimeMillis() - start) + " ms");
        return result;
    }
);

proxy.pay(250.0);

2. CGLIB / ByteBuddy (Subclass Proxying)

Generates dynamic subclasses of target concrete classes at the bytecode level:

  • Requirement: Does not require an interface; can wrap any standard class.
  • Constraints: Cannot proxy final classes or override final methods. Requires an accessible constructor.

Comparative Analysis: JDK Proxy vs CGLIB vs ByteBuddy

Criterion JDK Dynamic Proxy CGLIB ByteBuddy
Mechanism Interface implementation (implements Interface) Subclassing (extends TargetClass) Subclassing or in-memory bytecode transformation
Interface Required? Strictly Mandatory Not required Not required
final Methods / Classes Bypasses classes; intercepts interface methods Fails on final classes; cannot override final methods Cannot override final methods without Java Agent
Used In Spring AOP (classic), MyBatis mappers Hibernate lazy proxies, Spring Boot 2.x default Mockito 2+, Hibernate 5.3+, modern Spring Boot
Java Module (JPMS) Safety Native JDK support Deprecated / reflection warnings on Java 17+ Fully modular and compatible with modern JDKs

Spring Boot Evolution: proxyTargetClass

  • Classic Spring Framework (Spring 4.x/5.x): Defaults to proxyTargetClass = false. If a bean implemented any interface, Spring generated a JDK Dynamic Proxy. Injecting the bean by its concrete class type caused a fatal BeanNotOfRequiredTypeException.
  • Spring Boot 2.0+ & 3.x: Sets spring.aop.proxy-target-class=true by default. Spring standardizes on CGLIB / ByteBuddy subclass proxies, allowing injection by either interface or concrete class.

🔴 Senior Level

The Self-Invocation Hazard in Spring AOP

Because Spring AOP weaves cross-cutting concerns (@Transactional, @Cacheable, @Async, @PreAuthorize) via dynamic proxies, calling an annotated method from within the same class (this.method()) bypasses the proxy entirely:

@Service
public class OrderService {

    public void processOrder(Order order) {
        // Business validation logic
        this.saveWithTransaction(order); // ❌ FAILS: @Transactional is completely bypassed!
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveWithTransaction(Order order) {
        orderRepository.save(order);
    }
}
Caller ──> [ Spring AOP Proxy ] ──> OrderService.processOrder()
                                            │
                                            └─ (this.saveWithTransaction) ──> [ Direct Memory Call ]
                                               (Proxy interceptor chain is completely skipped!)

4 Production Remediation Strategies:

  1. Architectural Extraction (Best Practice): Move the transactional method into a dedicated collaborator bean.
  2. Self-Injection:
    @Service
    public class OrderService {
        @Autowired
        private OrderService self; // Injects the Spring proxy wrapper
    
        public void processOrder(Order order) {
            self.saveWithTransaction(order); // ✅ Routes through proxy!
        }
    }
    
  3. AopContext.currentProxy(): Requires @EnableAspectJAutoProxy(exposeProxy = true):
    ((OrderService) AopContext.currentProxy()).saveWithTransaction(order);
    
  4. Compile-Time / Load-Time Weaving (AspectJ): The AspectJ compiler (ajc) weaves bytecode directly into methods. Calls to this.method() are intercepted without dynamic proxies.

Why @Transactional Cannot Intercept private or final Methods

  • private Methods: Proxies cannot intercept them. JDK Proxies only know interface methods (which cannot be private). CGLIB cannot override private methods in subclasses. Spring AOP ignores annotations on private methods silently.
  • final Methods: CGLIB cannot override final methods. Invoking a final method on a CGLIB proxy routes directly to the superclass method, bypassing interceptors.

Hibernate Entity Lazy Proxies

When Hibernate executes entityManager.getReference(User.class, id) (or session.load()), it does not issue a SQL SELECT query. Instead, it creates a Virtual Proxy (via ByteBuddy) containing only the primary key id.

  • The SQL query is executed only when a non-ID getter (user.getName()) is invoked.
  • If the Hibernate session is closed before the proxy is initialized, accessing non-ID fields triggers the notorious LazyInitializationException: could not initialize proxy - no Session.

JVM Metaspace & JIT Inlining Impact

  1. Metaspace Allocation: Generated proxy classes are stored in the JVM’s Metaspace. Unbounded creation of dynamic proxies without caching can lead to classloader leaks and OutOfMemoryError: Metaspace.
  2. JIT Devirtualization: HotSpot’s C2 compiler inlines monomorphic dynamic proxy calls directly. However, if multiple interceptor chains create megamorphic call sites, JIT disables inlining, falling back to indirect vtable/itable dispatch.

4 Tricky Questions

1. Why does @Transactional fail silently when called internally within the same class (this.method()), and how does AspectJ differ from Spring AOP?

Answer: Spring AOP is built on runtime dynamic proxies. When an outside caller invokes a bean, it calls the outer proxy wrapper, which initiates the database transaction before delegating to the target instance. Once execution is inside the target bean, calling this.method() is a direct pointer invocation inside the JVM heap; it completely bypasses the outer proxy and its interceptor chain. In contrast, AspectJ modifies class bytecode directly during compilation (Compile-Time Weaving) or class loading (Load-Time Weaving). It inserts the transactional advice directly into method bytecodes, allowing AspectJ to intercept internal this.method() calls, constructors, and private methods without proxies.


2. Can Spring AOP intercept private or final methods using JDK Dynamic Proxy or CGLIB?

Answer: No. Neither mechanism can intercept private or final methods:

  • private methods: JDK Dynamic Proxy requires public interface methods. CGLIB creates a subclass, and Java bytecode rules forbid a subclass from overriding private methods of its superclass.
  • final methods: Java prohibits subclasses from overriding methods marked final. If CGLIB wraps a class with a final method, calling that method bypasses the proxy subclass interceptor and executes the original method directly without transactional or caching advice.

3. How does Hibernate implement lazy loading using the Virtual Proxy pattern, and what triggers a LazyInitializationException?

Answer: Hibernate uses ByteBuddy to generate a subclass proxy of the entity (e.g., User$HibernateProxy$xyz). The proxy stores the target entity ID and an uninitialized persistence interceptor. When client code accesses user.getId(), Hibernate returns the cached ID without querying the database. When any other field (e.g., user.getEmail()) is accessed, the proxy interceptor checks if the entity is loaded; if not, it queries the active Session and populates the entity. If the Hibernate Session / EntityManager has already been closed or the transaction committed, the interceptor cannot obtain a JDBC database connection, resulting in a LazyInitializationException.


4. Why did Spring Boot 2.0 change the default proxying mechanism to proxyTargetClass = true (CGLIB/ByteBuddy)?

Answer: In classic Spring (proxyTargetClass = false), beans implementing interfaces were proxied via JDK Dynamic Proxies. If a developer tried to inject a bean by its concrete implementation type (@Autowired private UserServiceImpl service), Spring threw a BeanNotOfRequiredTypeException because the JDK proxy class ($Proxy0) implemented UserService, but was not a subclass of UserServiceImpl. By defaulting proxyTargetClass = true, Spring Boot generates CGLIB/ByteBuddy subclass proxies, allowing beans to be injected interchangeably by either their interface or their concrete class type.


🎯 Interview Cheat Sheet

30-Second Elevator Pitch

“The Proxy pattern provides an interceptor or surrogate that controls, restricts, or delays access to a target object while preserving its interface. The primary types are Virtual (lazy loading), Protection (security checks), Remote (RPC/Feign), and Smart/Interceptor (Spring AOP transactions and caching). In Java, proxies are generated dynamically using JDK Dynamic Proxy (interface-based) or CGLIB/ByteBuddy (subclass-based). The most critical production pitfall is Self-Invocation, where internal calls via this.method() bypass the Spring AOP proxy, silently disabling @Transactional and @Async.”


Key Architectural Takeaways

  • JDK Proxy: Native, interface-only, reflection-based (InvocationHandler).
  • CGLIB / ByteBuddy: Subclasses concrete classes, cannot intercept final methods or final classes, default in Spring Boot 2.x+.
  • Self-Invocation Hazard: Calling this.method() bypasses the proxy; fix via class extraction or self-injection.
  • Private Methods: Never place @Transactional or @Async on private methods; proxies cannot intercept them.
  • Hibernate Virtual Proxy: Postpones SQL SELECT until non-ID fields are accessed; throws LazyInitializationException if the session is closed.

Red Flags & Anti-Patterns

  • 🚩 Assuming @Transactional works on internal this.method() calls.
  • 🚩 Placing Spring AOP annotations on private or final methods.
  • 🚩 Marking Spring service classes or transactional methods as final when using CGLIB.
  • 🚩 Confusing Proxy with Decorator or Adapter: Proxy controls access; Decorator adds new behavior; Adapter changes the interface.