Spring @Transactional: The Hidden Trap

Lakshay ChhabraLakshay Chhabra
3 min read

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

  1. Avoid self‑invocation of transactional methods. If possible, extract transactional logic into another service.

  2. Keep transactions short—only wrap database operations within the @Transactional boundary.

  3. Handle exceptions correctly—ensure you’re throwing the right exceptions (RuntimeException, Error, etc.) to trigger rollback.

  4. Use Propagation and Isolation 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.

0
Subscribe to my newsletter

Read articles from Lakshay Chhabra directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Lakshay Chhabra
Lakshay Chhabra