Spring @Transactional: The Hidden Trap

Why your @Transactional
Code Is Lying to You
By Lakshay Chhabra
Introduction
You’ve annotated your service method with @Transactional
, expecting Spring to handle rollbacks for any exceptions. Yet, to your surprise, some database changes sneak through. What gives? Let’s uncover the proxy magic (and pitfalls) behind Spring’s transaction management so you never lose data integrity again.
The Symptom
“I threw a RuntimeException inside my
@Transactional
method, but half the inserts still happened!”
You confidently trusted Spring’s @Transactional
—only to find inconsistent state in your tables. This often boils down to how Spring creates those transactional proxies.
Why It Happens: Self‑Invocation Bypasses the Proxy
Spring wraps beans in a proxy that intercepts calls to methods annotated with @Transactional
. But if you call a transactional method from within the same class, you’re calling it directly—bypassing the proxy entirely.
Imagine you have a service with multiple methods, one of which is transactional:
Copy
Copy
@Service
public class OrderService {
@Transactional
public void placeOrder(Order order) {
saveOrder(order);
chargeCustomer(order); // throws RuntimeException
}
private void chargeCustomer(Order order) {
// This internal method bypasses Spring’s proxy → no rollback
// any exception here won’t trigger the rollback on placeOrder()
}
}
Despite having @Transactional
on the placeOrder()
method, calling chargeCustomer()
within the same class bypasses Spring’s transaction management. As a result, the exception in chargeCustomer()
doesn’t trigger a rollback for placeOrder()
, and changes are committed to the database.
The Proxy Solution: Call External Beans
In Spring, transactional proxies only work if method calls go through them. Therefore, to ensure that transactions are correctly managed, avoid calling transactional methods from within the same class. Instead, delegate transactional logic to another service (or bean).
Here’s how you can refactor your code:
Copy
Copy
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
@Transactional
public void placeOrder(Order order) {
saveOrder(order);
paymentService.chargeCustomer(order); // now goes through the proxy
}
}
@Service
public class PaymentService {
@Transactional // optional if placeOrder() is already transactional
public void chargeCustomer(Order order) {
// exceptions here will now correctly roll back the entire transaction
}
}
By delegating the call to chargeCustomer()
to a separate bean, you ensure that the proxy intercepts the call and handles transaction rollback correctly.
Best Practices for Using @Transactional
in Spring
Avoid self‑invocation of transactional methods. If possible, extract transactional logic into another service.
Keep transactions short—only wrap database operations within the
@Transactional
boundary.Handle exceptions correctly—ensure you’re throwing the right exceptions (
RuntimeException
,Error
, etc.) to trigger rollback.Use
Propagation
andIsolation
carefully for handling complex transaction requirements.
Conclusion
The @Transactional
annotation may appear to be a simple solution for managing transactions, but it has a few quirks. By understanding how Spring's proxies work and following the best practices outlined above, you can ensure that your transactions roll back properly when things go wrong.
Subscribe to my newsletter
Read articles from Lakshay Chhabra directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
