A Thorough Exploration of the Internal Mechanism
Many developers know that in Spring Boot, you can simply place @Transactional
on a method and get declarative transactions. But few realize when and how Spring spots that annotation and what it does behind the scenes to transform your class into a transaction-aware proxy.
In this article, we’ll take a step-by-step look at:
- Which components detect
@Transactional
metadata during bean registration. - Exactly how the BeanPostProcessor phase constructs a proxy to wrap your method calls.
- The order of operations that leads from “class with an annotation” → “fully proxied bean.”
This deeper insight helps you confidently handle or debug transactional issues in any environment—monolith or MSA—because the underlying AOP mechanism remains consistent.
1) Big-Picture Flow: From Scanning to Proxy Registration
Let’s outline the general lifecycle of a bean under Spring Boot:
- Scanning (usually via
@SpringBootApplication
)- Spring scans packages for classes annotated with
@Service
,@Component
, etc. - Each discovered class becomes a BeanDefinition, storing metadata about which class to instantiate and how.
- Spring scans packages for classes annotated with
- Instantiation & Dependency Injection
- For each BeanDefinition, Spring calls the constructor (optionally injecting constructor arguments) or sets fields.
- At this point, we have a real instance of your class.
- BeanPostProcessor Phase
- After creation but before the bean is fully “initialized,” Spring calls each BeanPostProcessor.
- One specialized post-processor is the AutoProxyCreator for transaction AOP, which checks if the bean or its methods have transaction attributes.
- Advisor & Interceptor
- If the AutoProxyCreator sees that “yes,
@Transactional
metadata is present,” it sets up a TransactionInterceptor and wraps the bean in a proxy. - That proxy is then registered in the Spring container in place of the original.
- If the AutoProxyCreator sees that “yes,
- Runtime
- Whenever you call a method on that bean, the proxy intercepts the call, begins or joins a transaction, invokes the real method, and finally decides commit or rollback.
Everything revolves around the BeanPostProcessor scanning for @Transactional
, then building a proxy. Let’s see exactly who does this scanning and how they inject transaction logic.
2) Key Classes: AnnotationTransactionAttributeSource, Advisor, and AutoProxyCreator
2.1) AnnotationTransactionAttributeSource
- Role: It reads the
@Transactional
annotation from class or method metadata and converts it into a TransactionAttribute object containing propagation, isolation, timeout, rollback rules, etc. - Implementation: Usually AnnotationTransactionAttributeSource uses reflection and annotation parsing to gather everything you wrote in
@Transactional(...)
.
2.2) BeanFactoryTransactionAttributeSourceAdvisor
- Role: This is a special Advisor that knows “If a method or class has transaction attributes (from AnnotationTransactionAttributeSource), attach the TransactionInterceptor.”
- Pointcut: The advisor internally sets a pointcut that matches any method recognized by the TransactionAttributeSource.
- Advice: The advice is the TransactionInterceptor itself.
2.3) TransactionInterceptor
- Role: The interceptor that actually starts or joins a transaction before calling the real method, then commits or rolls back after.
- Implementation: It consults the
PlatformTransactionManager
(likeJpaTransactionManager
) to dogetTransaction(...)
, thencommit(...)
orrollback(...)
. - Method: If your method throws an exception listed in the rollback rules,
rollback
is invoked; otherwise it commits.
2.4) The AutoProxyCreator (BeanPostProcessor)
- Examples:
AnnotationAwareAspectJAutoProxyCreator
orInfrastructureAdvisorAutoProxyCreator
. - Role: A BeanPostProcessor that, after your bean is instantiated, checks with the
BeanFactoryTransactionAttributeSourceAdvisor
: “Should we apply the TransactionInterceptor to this bean?” If yes, it creates a JDK or CGLIB proxy. - When: Usually in
postProcessAfterInitialization(...)
.
So effectively, the AutoProxyCreator looks at your bean once it’s constructed, sees if it qualifies for transaction AOP, and if so, returns a proxy object that’s now “transaction-aware.”
3) Detailed Step-by-Step: Detecting @Transactional
and Creating the Proxy
Let’s illustrate the detailed sequence:
- Spring Boot starts:
- You have a
@SpringBootApplication
, along withspring-boot-starter-data-jpa
or another starter that triggers transaction auto-configuration.
- You have a
- TransactionInfrastructure is auto-configured:
- A
PlatformTransactionManager
bean is created (e.g.,JpaTransactionManager
). - A BeanFactoryTransactionAttributeSourceAdvisor is registered, pointing to the above manager.
- An AutoProxyCreator (like
AnnotationAwareAspectJAutoProxyCreator
) is also registered.
- A
- Component Scan:
- Spring finds, for example,
@Service class OrderService
with some@Transactional
methods. - Registers a BeanDefinition for
OrderService
.
- Spring finds, for example,
- Bean Creation:
- Spring calls
new OrderService(...)
, injects dependencies, etc. Now you have a rawOrderService
object.
- Spring calls
- BeanPostProcessor (AutoProxyCreator):
- postProcessAfterInitialization runs after the bean is “initialized” but before it’s finalized in the container.
- The auto-proxy logic consults the
BeanFactoryTransactionAttributeSourceAdvisor
. - Advisor checks
AnnotationTransactionAttributeSource
: “Does OrderService or its methods have@Transactional
attributes?” - If yes, it decides, “We should apply TransactionInterceptor to these methods.”
- AutoProxyCreator then creates either:
- a JDK dynamic proxy if
OrderService
implements an interface, - or a CGLIB subclass if not (or if
proxyTargetClass=true
).
- a JDK dynamic proxy if
- It returns that proxy, replacing the original bean in the BeanFactory.
- Runtime Calls:
- Now, whenever something calls
orderService.placeOrder()
, that call hits the TransactionInterceptor via the newly created proxy. - The interceptor fetches or starts a transaction, calls the real
placeOrder()
, then commits or rolls back upon completion.
- Now, whenever something calls
4) Replacing the Original Bean with a Proxy
Once Spring’s BeanPostProcessor (the auto-proxy creator) decides to apply transaction logic to a bean, it swaps that bean out with a proxy. This ensures all calls to its public methods go through the TransactionInterceptor. Let’s break down how that final “proxy replacement” step works—and why it can differ between JDK dynamic proxies and CGLIB.
4.1) The BeanPostProcessor Pipeline
After Spring instantiates your class and injects dependencies, it calls the postProcessAfterInitialization method of each BeanPostProcessor:
[Scan & register BeanDefinition]
↓
[Instantiate Bean & Inject Dependencies]
↓
[BeanPostProcessor (AutoProxyCreator) checks for @Transactional]
↓
[If needed, create a Proxy with TransactionInterceptor]
↓
[Replace original bean with this proxy in the BeanFactory]
↓
[Bean is fully initialized]
If the Advisor (e.g. BeanFactoryTransactionAttributeSourceAdvisor
) detects @Transactional
, the AutoProxyCreator steps in. It doesn’t alter your original bean directly. Instead, it returns a proxy object that delegates to your bean internally. Spring then registers that proxy in the container.
From your code’s perspective, you still get an instance named, say, orderService
, but behind the scenes, it’s actually a proxy with interceptors for transaction logic.
4.2) JDK vs. CGLIB: Two Ways to Build the Proxy
Spring can create the transactional proxy in two main ways:
- JDK Dynamic Proxy
- Relies on the bean implementing an interface.
- The generated proxy object implements that interface, forwarding calls to the real bean.
- Limitation: Only the methods defined by the interface are intercepted. Any method on the concrete class not declared in the interface is ignored by AOP.
- Historically, if a bean has an interface, Spring tries JDK dynamic proxies. In modern Spring Boot, you can override this by setting
proxyTargetClass=true
, forcing CGLIB even if an interface is present.
- CGLIB Proxy
- Subclasses your class at runtime.
- It overrides public (or protected) methods to insert transaction interceptors, then calls
super
on your real code. - Limitation: If the class or the method is
final
, it can’t be overridden, so no transaction logic is applied. Also, private methods are invisible to CGLIB. - If your class doesn’t implement any interface, or if you explicitly set
proxyTargetClass=true
, Spring uses CGLIB.
Why does it matter?
- JDK approach might skip AOP on methods you forgot to put in an interface.
- CGLIB approach can’t override
final
methods or classes, so@Transactional
on a final method does nothing.
In typical practice, either method is fine if you keep these limitations in mind. If you code to interfaces, you can let JDK proxies handle it—but ensure all relevant methods appear in the interface. If you avoid interfaces, or want class-based proxying, you can rely on CGLIB.
4.2.1) Example: JDK Proxy with Interface
public interface OrderService {
void placeOrder(String productId);
}
@Service
public class OrderServiceImpl implements OrderService {
@Override
@Transactional
public void placeOrder(String productId) {
// ...
}
}
- Here, JDK dynamic proxies easily handle it. But if you add a new public method that isn’t declared in
OrderService
(the interface), that method won’t be intercepted by the transaction proxy.
4.2.2) Example: CGLIB with proxyTargetClass=true
@SpringBootApplication
@EnableTransactionManagement(proxyTargetClass = true)
public class MyApp { ... }
@Service
public class PaymentService {
@Transactional
public final void pay(...) { ... } // Problem: final method => can't be overridden
}
- Because the method is
final
, CGLIB can’t override it, so no transaction logic. - If you remove
final
, CGLIB can override the method. This is crucial to avoid confusion: “Why did@Transactional
do nothing?”
4.3) Conclusion of the Proxy Replacement
Once the auto-proxy logic decides which approach (JDK or CGLIB) to use, it generates the proxy class at runtime and returns that from its post-processor method. The BeanFactory effectively registers “Proxy(OrderService)” or “Proxy(PaymentService)” in place of the original. As a result, every bean that depends on OrderService
or PaymentService
sees the proxy. When your code calls service.doSomething()
, the call flows:
- Proxy intercepts → checks for transaction attributes, calls
PlatformTransactionManager.getTransaction(...)
. - Calls the real method on the original bean instance.
- On method completion, commits or rolls back.
By the time the rest of your application runs, you only ever deal with this transactionally aware proxy object, never the raw bean. That’s the essence of how @Transactional
seamlessly enforces an all-or-nothing contract on your methods.
5) Under the Hood of TransactionInterceptor
While our main focus is the detection and proxy creation, it’s worth noting:
- TransactionInterceptor obtains a
TransactionAttribute
fromAnnotationTransactionAttributeSource
, specifying propagation, isolation, etc. - It calls the
PlatformTransactionManager.getTransaction(...)
to start or join a transaction. - After invoking the real method, it checks for exceptions. If a relevant exception is thrown,
rollback(...)
is called; otherwisecommit(...)
. - The rest of your code is none the wiser—it simply experiences an “all-or-nothing” effect.
6) Why Understanding This Mechanism Matters
Knowing that @Transactional
is recognized at the BeanPostProcessor stage—and that a proxy is built to intercept method calls—empowers you to:
- Troubleshoot “Why is my transaction not triggering?” (maybe it’s not scanned, not public, or a final method).
- Realize internal calls (
this.someMethod()
) bypass the proxy if you truly need separate transaction boundaries. - Understand logs or debug output related to “advisor found no methods” or “Creating transactional proxy for bean.”
This architecture is also the foundation for any additional AOP-based features in Spring. Once you see how the container decides, “Yes, we need an interceptor on these methods,” you’re better equipped to refine or debug your transaction usage—without guesswork.
Conclusion: @Transactional
Is a BeanPostProcessor + Proxy Decision
What appears as a “simple annotation” in code is actually a multi-step detection and proxy-creation process:
- Spring scans your service class during component scanning.
- Bean creation proceeds normally.
- A BeanPostProcessor (the AutoProxyCreator) sees if the bean or its methods carry
@Transactional
attributes via the Advisor. - If yes, a proxy is generated with the TransactionInterceptor—replacing the original bean.
- Runtime calls route through that proxy, which coordinates with the
PlatformTransactionManager
.
That’s how Spring Boot can let you merely slap on an annotation yet guarantee that each method is part of a managed, consistent transaction. No large servers or manual transaction boilerplate needed—just a carefully orchestrated “detect, proxy, intercept” cycle behind the scenes.