You’ve configured your @Transactional
settings—propagation, isolation, rollback rules—and verified everything in your code. Yet, sometimes you find a transaction isn’t firing, or a rollback isn’t happening. Why? This chapter examines three frequent culprits and some handy troubleshooting approaches.
1) Most Common Causes
1.1) Bean Not in Spring’s Scope → No Proxy Created
- Out of @ComponentScan range: If your class is outside the packages that Spring scans, Spring won’t manage that class as a bean—so it can’t create a transaction proxy.
- Manually instantiated (via
new
): If you instantiate an object on your own, Spring has no chance to wrap it in a proxy. - Result:
@Transactional
does nothing because there’s no AOP intercepting calls.
Example:
// Suppose you do this in code:
OrderService service = new OrderService();
// This is a plain instance, no Spring proxy -> no transaction logic
service.placeOrder(...);
To fix this, ensure you annotate it as a Spring bean (@Service
, etc.) and keep it in the scanned package structure. Let Spring handle object creation and injection.
1.2) Method Is private
/final
/static
→ Can’t Be Proxied
- private methods can’t be overridden even by CGLIB subclassing; JDK proxies only intercept public interface methods.
- final methods also block CGLIB from overriding them.
- static is purely a class-level function with no instance context to wrap.
If you put @Transactional
on any such method, it won’t be intercepted by the proxy—thus the transaction logic never triggers. If you truly need a transaction scope for that code, place it in a public method, or in a separate bean that calls it from outside.
1.3) Internal Calls: Proxy Bypass
- From the previous discussion on how AOP proxies work, calling another method in the same class (e.g.,
this.helperMethod()
) skips the proxy, preventing the second method’s transaction settings (e.g.,REQUIRES_NEW
) from activating. - If you want a separate transaction in a sub-operation, you typically must call it from a different bean or some external injection method (like self-injection or AspectJ advanced weaving).
Typical Example:
@Service
public class OrderService {
@Transactional
public void placeOrder(...) {
// This method is covered by a transaction
this.auditLog(...);
// Internal call => no new transaction triggered
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void auditLog(...) {
// Expectation: brand-new transaction
// Reality: call is internal, bypasses proxy
}
}
2) Solutions or Debugging Tips
- Check If the Proxy Is Created
- Enable DEBUG logs for
org.springframework.aop
ororg.springframework.transaction
: look for “Creating proxy for bean…” or “Advised methods:” messages. If you don’t see them, that bean isn’t being proxied. - Tools like Spring Boot Actuator can show if a bean is actually a proxy class.
- Enable DEBUG logs for
- Verify @ComponentScan / @SpringBootApplication Ranges
- Confirm the bean’s package is included in scanning. A frequent mistake is placing classes outside the “main” scanned package, so they aren’t recognized as beans.
- Public Methods for Transaction, or Move Code to Another Bean
- If a method must get its own transaction, ensure it’s public and externally called. If you keep it private or final, Spring can’t advise it.
- If you’re calling from the same class, consider splitting the logic into a separate Service so calls become external and pass through the second bean’s proxy.
- Check for Self-Invocation
- If you do
this.someMethod()
internally, it never crosses the proxy boundary. If you want a different transaction scope, do a separate bean call or advanced solutions like self-injection or AspectJ.
- If you do
Conclusion
When transaction settings don’t seem to apply, the usual suspects are:
- Bean never got proxied, because it’s outside scanning or manually instantiated.
- Method is private/final/static, so the proxy can’t intercept it.
- Internal calls bypass the proxy, so new transaction rules or readOnly settings won’t kick in.
Debug by checking proxy creation logs, ensuring it’s a public method inside a Spring-managed bean, and confirm calls are made from outside the bean if you need a distinct transaction scope. If advanced scenarios arise, advanced AOP techniques (AspectJ, self-reference) or separate beans are your best bet.
Understanding these core proxy limitations will help you quickly spot why @Transactional
“isn’t working” and guide you to the right fix—whether that’s adjusting your package structure, changing method signatures, or refactoring code to use external bean calls where you truly want a separate transaction.