Definition
- AOP Proxy – Spring wraps the bean in a proxy that intercepts method calls and manages the transaction lifecycle (begin, commit, rollback).
- JDK Dynamic Proxy – created by
java.lang.reflect.Proxy; works only when the bean implements an interface; reads annotations from the interface methods. - CGLIB Proxy – generates a subclass of the concrete class at runtime; does not require an interface; reads annotations from the implementation class.
- Spring Boot default – since Spring 5 and Spring Boot 2,
proxyTargetClass = trueis the default, meaning CGLIB is used even when interfaces are present. - Recommendation – always annotate the implementation so the annotation works under both proxy types; keep classes and transactional methods non-final and public.
At the National Bank of India, the proxy is a security guard standing in front of the real clerk. With a JDK proxy, the guard reads the rulebook posted on the public counter (the interface). With a CGLIB proxy, the guard is a stand-in wearing the clerk's uniform (a subclass) who reads the rules from the clerk's own notebook (the implementation). Spring Boot hires the uniform-wearing stand-in by default, so the transaction rule must be in the clerk's notebook or it gets ignored.
Understanding the Concepts
What are the two proxy types in Spring?
Spring creates an AOP proxy around your bean to implement @Transactional. There are two proxy mechanisms.
A JDK dynamic proxy is created by java.lang.reflect.Proxy. It works only when your bean implements at least one interface because the proxy itself implements those interfaces. It reads transaction metadata from the interface methods.
A CGLIB proxy is created by generating a subclass of your concrete class at runtime. It does not need an interface because it extends the class directly. It reads transaction metadata from the concrete class and its methods.
package com.bank.service;
// Interface
public interface TransferService {
void transfer(String fromId, String toId, java.math.BigDecimal amount);
}
// Implementation
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class TransferServiceImpl implements TransferService {
private final AccountRepository accountRepo;
public TransferServiceImpl(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
@Transactional(rollbackFor = Exception.class)
@Override
public void transfer(String fromId, String toId, BigDecimal amount) {
Account sender = accountRepo.findByIdForUpdate(fromId);
Account receiver = accountRepo.findByIdForUpdate(toId);
sender.debit(amount);
receiver.credit(amount);
accountRepo.save(sender);
accountRepo.save(receiver);
}
}
Why does Spring Boot default to CGLIB?
Historically, Spring used JDK proxies when the bean had an interface and fell back to CGLIB only when there was no interface. Since Spring 5 and Spring Boot 2, the default changed to proxyTargetClass = true, meaning CGLIB is used even when interfaces are present.
The reason is consistency: with CGLIB as the default, beans behave the same regardless of whether they implement an interface. You inject the concrete type directly without worrying about type casting issues.
package com.bank.controller;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.PostMapping;
import java.math.BigDecimal;
@RestController
public class TransferController {
// With CGLIB, you can inject the concrete type directly
private final TransferServiceImpl transferService;
public TransferController(TransferServiceImpl transferService) {
this.transferService = transferService;
}
@PostMapping("/transfer")
public String transfer() {
transferService.transfer("SAV2001", "SAV3002",
new BigDecimal("50000"));
return "Transfer complete";
}
}
How do JDK and CGLIB proxies compare?
| Aspect | JDK Dynamic Proxy | CGLIB Proxy |
|--------|-------------------|-------------|
| Mechanism | Implements interfaces | Subclasses concrete class |
| Interface required | Yes | No |
| Reads annotations from | Interface methods | Implementation class |
| final class | Works (proxy is separate) | Fails (cannot subclass final) |
| final method | Proxied (if on interface) | Not proxied |
| Private method | Not proxied | Not proxied |
| Spring Boot default | No | Yes (proxyTargetClass=true) |
| Injection type | Must inject interface type | Can inject concrete type |
package com.bank.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
// To force JDK proxies (not recommended for Spring Boot):
@Configuration
@EnableTransactionManagement(proxyTargetClass = false)
public class JdkProxyConfig {
// Beans implementing interfaces will use JDK dynamic proxy
// Beans without interfaces still use CGLIB
}
Why should @Transactional go on the implementation?
A CGLIB proxy subclasses the implementation. It reliably sees @Transactional on the implementation class and its methods. It does not reliably pick up annotations that exist only on the interface, because CGLIB operates at the class level.
If you put @Transactional only on the interface and Spring uses CGLIB, the annotation can be silently ignored. No transaction starts, and your transfer method runs without atomicity. In a banking application, this means a failing transfer could leave money half-moved.
package com.bank.service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
// WRONG: annotation on the interface, CGLIB may ignore it
public interface TransferService {
@Transactional
void transfer(String fromId, String toId, BigDecimal amount);
}
// Implementation has no annotation
import org.springframework.stereotype.Service;
@Service
public class TransferServiceImpl implements TransferService {
private final AccountRepository accountRepo;
public TransferServiceImpl(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
@Override
public void transfer(String fromId, String toId, BigDecimal amount) {
// No transaction: debit succeeds, credit fails,
// Rajesh loses Rs.50,000 and Priya never receives it
Account sender = accountRepo.findByIdForUpdate(fromId);
Account receiver = accountRepo.findByIdForUpdate(toId);
sender.debit(amount);
receiver.credit(amount);
}
}
// CORRECT: annotation on the implementation
public interface TransferService {
void transfer(String fromId, String toId, BigDecimal amount);
}
@Service
public class TransferServiceImpl implements TransferService {
private final AccountRepository accountRepo;
public TransferServiceImpl(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
@Transactional(rollbackFor = Exception.class)
@Override
public void transfer(String fromId, String toId, BigDecimal amount) {
Account sender = accountRepo.findByIdForUpdate(fromId);
Account receiver = accountRepo.findByIdForUpdate(toId);
sender.debit(amount);
receiver.credit(amount);
}
}
What about final classes and methods?
CGLIB creates a subclass, so the target class must not be final. A final class cannot be subclassed, and Spring will fail at startup or fall back silently. Similarly, a final method cannot be overridden by the CGLIB subclass, so its @Transactional is ignored.
package com.bank.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
// WRONG: final class cannot be proxied by CGLIB
@Service
public final class FinalTransferService {
@Transactional
public void transfer(String fromId, String toId, BigDecimal amount) {
// CGLIB cannot create a subclass of a final class
}
}
// WRONG: final method cannot be overridden by CGLIB
@Service
public class TransferService {
@Transactional
public final void transfer(String fromId, String toId, BigDecimal amount) {
// @Transactional is silently ignored
}
}
// CORRECT: non-final class and non-final method
@Service
public class TransferService {
@Transactional(rollbackFor = Exception.class)
public void transfer(String fromId, String toId, BigDecimal amount) {
// CGLIB can proxy this
}
}
What about private methods?
Neither JDK nor CGLIB proxies can intercept private methods. A @Transactional annotation on a private method is silently ignored. The method runs without a transaction.
package com.bank.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class FeeService {
// WRONG: private method, annotation ignored
@Transactional
private void calculateFee(String accountId) {
// runs without transaction
}
// CORRECT: public method, proxy intercepts
@Transactional
public void calculateFee(String accountId, java.math.BigDecimal rate) {
// runs inside a transaction
}
}
What are common mistakes?
Mistake 1: annotating only the interface
// WRONG: @Transactional on interface, CGLIB ignores it
public interface LoanService {
@Transactional
void disburseLoan(String loanId);
}
@Service
public class LoanServiceImpl implements LoanService {
@Override
public void disburseLoan(String loanId) { /* no TX */ }
}
// CORRECT: @Transactional on implementation
public interface LoanService {
void disburseLoan(String loanId);
}
@Service
public class LoanServiceImpl implements LoanService {
@Transactional(rollbackFor = Exception.class)
@Override
public void disburseLoan(String loanId) { /* TX active */ }
}
Mistake 2: making the service class final (Kotlin default)
In Kotlin, classes are final by default. You must use the open keyword or the kotlin-allopen compiler plugin to make CGLIB proxying work.
// WRONG in Kotlin: class is final by default
@Service
class TransferService {
@Transactional
fun transfer(fromId: String, toId: String, amount: BigDecimal) { }
}
// CORRECT in Kotlin: open class
@Service
open class TransferService {
@Transactional
open fun transfer(fromId: String, toId: String, amount: BigDecimal) { }
}
What should a senior engineer know?
- The Spring Framework reference documentation itself recommends annotating concrete classes rather than interfaces because the choice of proxy strategy affects whether interface annotations are honored.
- You can force JDK proxies with
@EnableTransactionManagement(proxyTargetClass = false), but even then annotating the implementation is the defensive choice. - Spring's
AnnotationTransactionAttributeSourcedoes check interfaces as a fallback for JDK proxies, but this behavior is not guaranteed across proxy types. - In Spring Boot 3.x, CGLIB proxy generation is further optimized. The class must still be non-final.
- For Kotlin projects, use the
kotlin-springcompiler plugin (included in the Spring Boot Kotlin starter) which automatically opens classes annotated with@Service,@Component, and@Transactional.
// Check at runtime which proxy type Spring created
import org.springframework.aop.support.AopUtils;
public class ProxyCheck {
public static void checkProxy(Object bean) {
System.out.println("Is CGLIB: " + AopUtils.isCglibProxy(bean));
System.out.println("Is JDK: " + AopUtils.isJdkDynamicProxy(bean));
System.out.println("Proxy class: " + bean.getClass().getName());
}
}
Is CGLIB: true
Is JDK: false
Proxy class: com.bank.service.TransferServiceImpl$$SpringCGLIB$$0
Visual Overview
JDK Dynamic Proxy CGLIB Proxy (Spring Boot default)
────────────────── ──────────────────────────────────
┌──────────────────┐ ┌──────────────────────────────┐
│ Proxy (JDK) │ │ Proxy (CGLIB subclass) │
│ implements: │ │ extends: │
│ TransferService │ │ TransferServiceImpl │
│ (interface) │ │ (concrete class) │
│ │ │ │
│ Reads @TX from │ │ Reads @TX from │
│ INTERFACE │ │ IMPLEMENTATION │
└────────┬─────────┘ └──────────────┬───────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────────────────┐
│ TransferService │ │ TransferServiceImpl │
│ Impl (target) │ │ (target, same class) │
└──────────────────┘ └──────────────────────────────┘
Key Takeaway
Spring implements @Transactional through an AOP proxy that wraps the bean. There are two kinds of proxy: JDK dynamic proxy, which implements the bean's interfaces and reads annotations from interface methods, and CGLIB proxy, which generates a subclass of the concrete class and reads annotations from the implementation. Since Spring 5 and Spring Boot 2, CGLIB is the default (proxyTargetClass = true), even when the bean has an interface.
This default is why annotation placement matters. At National Bank of India, a developer once put @Transactional on the TransferService interface and left the TransferServiceImpl bare. Because Spring Boot used CGLIB, the annotation was silently ignored, no transaction started, and a failing transfer left money half-moved between Rajesh's account SAV2001 and Priya's account SAV3002. The fix is to always put @Transactional on the implementation class or its methods, keeping the interface clean.
CGLIB has a few additional rules. The class must not be final (cannot subclass), transactional methods must not be final (cannot override), and private methods are never proxied. In Kotlin, where classes are final by default, use the kotlin-spring compiler plugin which automatically opens annotated classes. The self-invocation rule applies under both proxy types: calling a transactional method on this bypasses the proxy.
Remember one sentence: always put @Transactional on the implementation, not the interface, because Spring Boot defaults to CGLIB proxies which read annotations from the concrete class, not the interface.
---