Mastering Spring & MSA Transactions – Part 11: Under the Hood of @Transactional: How Spring’s Proxy Actually Applies Your Rules

In our previous articles, we saw how to configure transaction boundaries (Propagation), handle concurrency through Isolation, specify rollback exceptions, and fine-tune performance with readOnly or timeout. But after setting all these @Transactional properties, a question remains:

How exactly does Spring apply those rules to each method call?

The short answer: AOP proxies. Whenever you annotate methods or classes with @Transactional, Spring automatically creates a proxy that intercepts the call, starts (or joins) the transaction, and performs commit/rollback logic when the method completes.

This article lifts the hood on that proxy mechanism, so you’ll understand precisely how Spring can weave in transaction logic without you writing explicit commit() or rollback() calls.

1) Why @Transactional Depends on AOP Proxies

Spring’s goal is to let developers define “transaction settings” with an annotation—without forcing them to manually code transaction boundaries. So it uses Aspect-Oriented Programming (AOP) to intercept your public methods:

  • Before the method runs, it starts or joins a transaction (depending on your Propagation and Isolation settings).
  • After the method finishes, it checks for exceptions or normal completion, deciding whether to commit or rollback.

But AOP in Spring is usually proxy-based: instead of the real bean, Spring registers a proxy bean that wraps your logic. All external calls pass through this proxy, allowing the transaction logic to be injected around the method invocation.

2) JDK vs. CGLIB: Two Ways to Create a Proxy

  1. JDK Dynamic Proxy
    • If the class implements at least one interface, Spring often creates a proxy that implements that interface.
    • The proxy delegates calls to the underlying real bean while adding transaction logic before/after the call.
  2. CGLIB (Class Inheritance)
    • If there’s no interface (or if proxyTargetClass=true is enabled), Spring uses class-based proxying, subclassing your class and overriding the methods to add transaction code.
    • Modern Spring Boot tends to default to CGLIB for simplicity, even if you have an interface, but historically JDK proxies were used if an interface was present.

The end result is similar: callers think they’re invoking myService.myMethod(), but they’re actually calling a proxy that runs the AOP logic around the real method.

3) How the Proxy Bean Replaces the Real One

During Spring’s bean initialization phase:

  1. Spring scans your classes for annotations like @Service, @Transactional, etc.
  2. If a bean is found to have @Transactional (class-level or method-level), a BeanPostProcessor (e.g., InfrastructureAdvisorAutoProxyCreator) decides “this bean needs an AOP proxy.”
  3. Instead of registering the bean itself, Spring registers a proxy instance as the final bean.
  4. Other beans that @Autowired or reference this bean actually get the proxy in the injection step.

Hence, any public method call from an external caller goes:

External Caller -> Proxy -> TransactionInterceptor -> Real Method

This is key to how @Transactional can handle DB commits/rollbacks automatically.

4) TransactionInterceptor: The Core Logic

When the proxy intercepts a call, it delegates to the TransactionInterceptor, which uses:

  • AnnotationTransactionAttributeSource: Reads the @Transactional metadata (propagation, isolation, rollbackFor, etc.) from the method/class.
  • PlatformTransactionManager: Actually starts or resumes a transaction (JDBC, JPA, etc.) and eventually commits/rolls back.
  • TransactionAttribute: Encapsulates the effective configuration for that method (timeout, readOnly, etc.).

Before the real method runs, the interceptor checks the configured attributes and begins or joins the transaction. After the method, it decides commit vs. rollback based on success or caught exceptions.

5) How This Relates to Your Configured Settings

All the transaction properties we’ve discussed—Propagation (e.g., REQUIRES_NEW), Isolation (REPEATABLE_READ), rollback rules (rollbackFor, noRollbackFor), readOnly, and timeout—get extracted when the proxy sees “a method with @Transactional.” The TransactionInterceptor references those properties for each call:

  1. Start transaction with the correct propagation/isolation.
  2. If the method hits an exception that triggers rollback (by default, runtime exceptions, or your custom rollbackFor config), it rolls back.
  3. If it completes normally, it commits (unless readOnly = true with certain advanced DB behaviors—rarely an actual rollback, but the concept stands).
  4. Timeout can cause forced rollback if the method runs too long.

6) Coming Up Next: Why Some Calls Don’t Trigger the Proxy

This entire mechanism only applies when:

  • The method is invoked externally through the bean’s proxy object.
  • The method is public (so it can be overridden or recognized by JDK proxy).

But what if you call a private method, or call a second method from within the same class (this.someMethod())—bypassing the external proxy path? That’s exactly why some transactions “don’t fire,” or custom settings like REQUIRES_NEW go ignored.

We’ll dive into those pitfalls—self-invocation, private/final methods, direct instantiation instead of letting Spring manage the bean—in the next article. Understanding the core proxy structure described here sets the foundation.

Conclusion

Spring’s @Transactional logic is AOP-based: a proxy stands in for your real bean, intercepting method calls to start or join a transaction and handle commits/rollbacks. This approach is powerful and flexible, letting you define everything with annotations instead of manual transaction code. However:

  1. The proxy only intercepts external calls from other beans or clients.
  2. Internal calls or certain method signatures (private/final) can’t be intercepted with the default setup.

In the next article, we’ll look specifically at when transactions “don’t work” because calls skip the proxy, or method signatures make AOP impossible—and how you can fix it if you really need separate transaction scopes in those scenarios.

For now, you should have a solid grasp of why @Transactional can do its work without cluttering your code with commits/rollbacks: behind the scenes, a proxy is weaving transaction logic into your method calls.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top