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)
- Virtual Proxy (Lazy Loading): Defers the instantiation of a heavy, memory-intensive object until the moment it is first invoked.
- Protection Proxy (Access Control & Security): Validates permissions, authentication, and authorization before forwarding the invocation to the real service.
- Remote Proxy (RPC / IPC): Represents an object located on a remote server, hiding network transport details (e.g., gRPC, Spring Cloud OpenFeign, RMI).
- Caching Proxy (Memoization): Caches the results of expensive operations and returns stored data for subsequent identical requests.
- 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
finalclasses or overridefinalmethods. 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 fatalBeanNotOfRequiredTypeException. - Spring Boot 2.0+ & 3.x: Sets
spring.aop.proxy-target-class=trueby 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:
- Architectural Extraction (Best Practice): Move the transactional method into a dedicated collaborator bean.
- 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! } } AopContext.currentProxy(): Requires@EnableAspectJAutoProxy(exposeProxy = true):((OrderService) AopContext.currentProxy()).saveWithTransaction(order);- Compile-Time / Load-Time Weaving (AspectJ): The AspectJ compiler (
ajc) weaves bytecode directly into methods. Calls tothis.method()are intercepted without dynamic proxies.
Why @Transactional Cannot Intercept private or final Methods
privateMethods: 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.finalMethods: CGLIB cannot overridefinalmethods. 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
- 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. - 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:
privatemethods: 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.finalmethods: Java prohibits subclasses from overriding methods markedfinal. 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@Transactionaland@Async.”
Key Architectural Takeaways
- JDK Proxy: Native, interface-only, reflection-based (
InvocationHandler). - CGLIB / ByteBuddy: Subclasses concrete classes, cannot intercept
finalmethods orfinalclasses, 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
@Transactionalor@Asyncon private methods; proxies cannot intercept them. - Hibernate Virtual Proxy: Postpones SQL
SELECTuntil non-ID fields are accessed; throwsLazyInitializationExceptionif the session is closed.
Red Flags & Anti-Patterns
- 🚩 Assuming
@Transactionalworks on internalthis.method()calls. - 🚩 Placing Spring AOP annotations on
privateorfinalmethods. - 🚩 Marking Spring service classes or transactional methods as
finalwhen using CGLIB. - 🚩 Confusing Proxy with Decorator or Adapter: Proxy controls access; Decorator adds new behavior; Adapter changes the interface.
Related Topics
- Advantage of Decorator over inheritance — Proxy vs Decorator comparison
- What is Iterator pattern — Behavioral traversal pattern
- What are design patterns — Core structural patterns
- What pattern categories exist — Structural patterns taxonomy