
Spring wraps your beans in proxies to add behaviour like transactions and caching without touching your code. Learn how JDK dynamic proxies and CGLIB proxies differ, how Spring chooses between them, and the traps you'll hit.
What Is a Spring Proxy?
A Spring proxy is a stand-in object that Spring creates at runtime and places between a caller and the target bean. Its purpose is to intercept invocations so that cross-cutting behavior—such as transaction management, caching, security, retries, or async execution—can run without modifying the bean’s own code. In the canonical example, a PaymentService interface defines charge(...). RealPaymentService implements it. A LoggingPaymentService also implements the same interface, holds a PaymentService target, logs each call, then forwards to target.charge(...). Callers see the same interface; the wrapper adds behavior transparently. Spring automates this wrapping: you annotate a method, and Spring generates the proxy at runtime.
Because transactions, caching, and security apply across unrelated methods, they are cross-cutting concerns. Rather than repeating boilerplate in each method, Spring places the concern inside the proxy. The bean’s methods remain clean; the annotation’s behavior lives in the wrapper.
Spring builds proxies in two ways:
- JDK dynamic proxy: uses
java.lang.reflect.Proxyto generate a class that implements the bean’s interface. Each call is routed to anInvocationHandler. Works only if the bean has at least one interface. - CGLIB proxy: generates a subclass of the bean’s concrete class and overrides its methods. No interface is required.
Spring’s classic rule is: interface present → JDK proxy; no interface → CGLIB. You can force CGLIB with proxyTargetClass = true, and Spring Boot defaults to CGLIB for all beans. All proxy gotchas follow from two facts. First, only external calls pass through the proxy; a method calling another method internally via this does not, so @Transactional, @Cacheable, and @Async silently do nothing on self-invocation. Second, CGLIB cannot proxy final classes or methods, and neither mechanism can proxy private methods. Additionally, with a JDK proxy you must inject the interface, not the concrete class, because the generated object is not an instance of the class. Finally, CGLIB instantiation can run the constructor twice or skip initialization expectations; perform setup in @PostConstruct.
The one model to keep: a Spring proxy is a wrapper that adds behavior around your methods without changing their bodies, and every gotcha stems from whether the call actually enters through that wrapper.
JDK Dynamic Proxies: Interface-Based Wrappers
Java's java.lang.reflect.Proxy is a runtime class generator. Given a class loader and an array of interfaces, it synthesizes a new class that implements those interfaces and returns an instance of it. The resulting object is a JDK dynamic proxy: it has the same interface surface as the target, but no method bodies of its own. Every method call made through the proxy is intercepted and routed into a single callback method defined by the caller — an InvocationHandler.
PaymentService real = new RealPaymentService();
PaymentService proxy = (PaymentService) Proxy.newProxyInstance(
real.getClass().getClassLoader(),
new Class[]{ PaymentService.class },
(proxyObj, method, args) -> {
System.out.println("before " + method.getName());
Object result = method.invoke(real, args);
System.out.println("after " + method.getName());
return result;
});
When a caller invokes proxy.charge(...), Java dispatches the call to the handler lambda with the Method and argument array. The handler executes cross-cutting logic, forwards to the real object with method.invoke(real, args), and returns the result to the caller. This is the same shape as a hand-written wrapper class, except the wrapper is generated at runtime rather than compiled.
The decisive limitation is in the second argument to newProxyInstance: a JDK dynamic proxy can only mimic interfaces. The generated class implements PaymentService, but it is not a subclass of RealPaymentService — it is a synthetic type that merely shares the interface. Consequently:
- JDK dynamic proxies work only when the target bean has an interface to stand behind.
- In plain Spring Framework, a bean implementing at least one interface receives a JDK dynamic proxy by default; a bean without an interface receives a CGLIB subclass proxy instead.
- Because the proxy is not the concrete class, injecting the concrete type fails at startup. Inject the interface instead:
@Autowired PaymentService payments;
Spring Boot defaults to CGLIB for all beans, which sidesteps the concrete-injection failure, but JDK dynamic proxies remain fundamental to understanding Spring's proxying model. Use interface-based injection when JDK proxies are in play, keep proxied methods public and non-final, and note that calls from inside the bean to itself bypass the proxy entirely.
CGLIB Proxies: Subclassing Without Interfaces
When enterprise applications require cross-cutting concerns—such as transaction management, security, or caching—without cluttering business logic, they leverage proxy patterns. While JDK dynamic proxies are restricted to interface-based designs, CGLIB (Code Generation Library) provides a mechanism to generate proxies for concrete classes by manipulating bytecode at runtime.
CGLIB operates by generating a subclass of the target bean. Because the generated proxy is a subclass, it passes as an instance of the original class, satisfying type-safety requirements even when no interface exists. This "is-a" relationship distinguishes it from JDK proxies, which only share a common interface with the target.
The Enhancer and MethodInterceptor
To construct a proxy, developers utilize the Enhancer class. This component defines the target class to be extended and attaches a MethodInterceptor to handle method invocations. When a method is called on the proxy, the interceptor receives control, allowing for logic injection before or after the target execution.
The following example demonstrates the fundamental implementation:
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(RealPaymentService.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxyRef) -> {
// Logic before execution
Object result = proxyRef.invokeSuper(obj, args);
// Logic after execution
return result;
});
RealPaymentService proxy = (RealPaymentService) enhancer.create();
Key Technical Considerations
- invokeSuper: Within the interceptor, the
proxyRef.invokeSuper(obj, args)call is the mechanism that executes the original logic defined in the superclass. Failure to invoke this method prevents the underlying business code from executing. - No Interface Requirement: CGLIB is frequently the preferred choice for frameworks like Spring Boot because it does not require the target to implement an interface, making it more flexible for standard POJOs.
- Constraints on Subclassing: Since CGLIB generates a subclass and overrides methods, it cannot intercept
finalclasses orfinalmethods. Similarly,privatemethods cannot be overridden and thus remain unproxied. - Constructor Behavior: Because the proxy is a generated subclass, the original class's constructor may be invoked in non-standard ways. Enterprise engineers should avoid side effects in constructors, delegating initialization to
@PostConstructmethods instead.
How Spring Chooses: JDK vs CGLIB
Spring delivers cross-cutting behaviour—transactions, caching, security, asynchronous execution—through a runtime-generated proxy: a stand-in object that intercepts calls to your bean and forwards them to the real target. How Spring builds that proxy determines which types can be safely injected and which annotations actually take effect.
JDK dynamic proxies, provided by java.lang.reflect.Proxy, generate a new class that implements one or more interfaces. Every call is routed into an InvocationHandler. Because the generated proxy is an implementation of the interface, not of the concrete class, it can only be assigned to the interface type. CGLIB proxies work differently: they generate a subclass of the target class at runtime and override its methods through a MethodInterceptor. The proxy genuinely is the concrete type, so an interface is not required.
The classic rule in plain Spring Framework is:
- Bean implements at least one interface → JDK dynamic proxy against that interface.
- Bean has no interface → CGLIB subclass.
You can force CGLIB even when interfaces exist:
@EnableTransactionManagement(proxyTargetClass = true)
Spring Boot's default is CGLIB for everything. The rationale is practical: a JDK proxy exposes only the interface, so code that injects the concrete class fails at startup. A CGLIB subclass proxy is the concrete type, so both interface-based and class-based injection resolve.
Three consequences matter in daily design:
- Inject the interface when a JDK proxy is in play.
@Autowired RealPaymentServicefails;@Autowired PaymentServicesucceeds. - Self-invocation bypasses the proxy. When one method calls another internally, the call stays inside
this; the proxy never sees it, so@Transactionalon the inner method is silently ignored. - Final and private methods cannot be proxied. CGLIB cannot override them, leaving any annotation inert.
Keep proxied methods public and non-final, route internal annotated calls through a separate bean or a self-injected reference, and construct dependencies rather than placing side effects in constructors, which CGLIB may invoke unexpectedly during proxy creation.
Proxy Gotchas: Self-Invocation and Non-Overridable Methods
Spring implements @Transactional, @Cacheable, and @Async by placing a proxy between the caller and the target bean. Two mechanisms exist. A JDK dynamic proxy implements the bean's interface and routes every call through an InvocationHandler. A CGLIB proxy generates a subclass of the concrete class at runtime and overrides its methods. Spring Boot defaults to CGLIB, so the concrete type still resolves for injection.
The critical constraint: the proxy only intercepts calls that enter from outside the bean. When a method inside the same bean calls another method, it is a plain this call on the real object, underneath the proxy. The wrapper never sees it, so the annotation's behaviour is skipped.
@Service
public class Orders {
public void placeAll(List<Order> orders) {
for (Order o : orders) {
save(o); // this.save() — stays inside 'this'
}
}
@Transactional
public void save(Order o) { ... }
}
Every save call here runs without a transaction. Only calls from another bean, arriving through the proxy, are wrapped. @Cacheable and @Async behave the same way: self-invocation silently does nothing. To fix it, move the annotated method into a separate bean, or inject the bean into itself and call through that injected reference.
Non-overridable methods form the second trap. CGLIB works by subclassing and overriding. Java forbids overriding a final method or subclassing a final class, so CGLIB cannot wrap them — a @Transactional on a final method quietly does nothing. private methods fail for a related reason: a subclass cannot override them, and an interface cannot declare them, so neither proxy style can wrap them.
Practical rules:
- Keep proxied methods
publicand non-final. - Avoid self-invocation of annotated methods; call through a separate bean or a self-injected proxy reference.
- Do not put important side effects in constructors of proxied beans — with CGLIB the constructor can run twice, or field initialisers may not have run when expected; use
@PostConstructfor setup.
More Gotchas: Injection Types and Constructor Behavior
A proxied Spring bean is a stand-in object; the class you wrote is the target underneath it. The proxy's type therefore determines what you can inject. When Spring builds a JDK dynamic proxy—its classic choice for a bean that implements at least one interface—the generated class implements the interface but does not extend the concrete class. The proxy is assignable to the interface only.
@Autowired
RealPaymentService payments; // fails when a JDK proxy is in play
Spring matches injected fields against the proxy's type hierarchy. Since RealPaymentService is not in that hierarchy, the container cannot satisfy the dependency and startup fails. Inject the interface instead:
@Autowired
PaymentService payments; // fine: the proxy is-a PaymentService
This is why Spring Boot favors CGLIB proxies: a CGLIB proxy subclasses the concrete class, so injecting either form resolves. CGLIB, however, constructs its subclass through a non-standard route. It does not call the constructor the way normal instantiation does. The practical consequences are:
- The constructor may run twice.
- Field initializers might not run at the expected point in the lifecycle.
- Important state may be unset when the proxy instance first appears.
Therefore, do not perform critical setup or side effects in the constructor of a bean that can be proxied. Defer that work to a method annotated with @PostConstruct, which Spring calls once on the fully built instance:
@Component
public class OrderService {
public OrderService(Dependency dep) {
this.dep = dep; // simple assignment only
}
@PostConstruct
public void init() {
// validation, cache warming, or registration belongs here
}
}
For enterprise code, follow two rules: inject the interface when a JDK dynamic proxy may be present, and keep constructors free of logic that must run exactly once in a predictable order.
Editorial Policy & Research Methodology
Our findings are based on rigorous internal research, verified industry benchmarks, and direct technical implementation experience from our enterprise client projects. All statistics and technical claims are reviewed by senior engineers before publication to ensure accuracy, transparency, and helpfulness for our readers.
