Why Spring @Transactional Sometimes Fails: 12 Common Pitfalls and Fixes
This article explains how Spring's declarative transaction management works, why the @Transactional annotation can unexpectedly become ineffective, and presents twelve typical scenarios—such as internal method calls, non‑public methods, final/static modifiers, proxy issues, and incorrect propagation settings—along with practical solutions to ensure reliable transaction handling.
Spring provides both declarative and programmatic transaction management; declarative transactions are implemented via Spring's AOP, which dynamically weaves transaction logic (resource binding, transaction start, commit/rollback, exception conversion) into the appropriate join points of business methods.
With Spring Boot's auto‑configuration, adding @Transactional to a method enables transaction support with a single annotation, but several hidden pitfalls can cause the transaction to be ignored.
1. Internal method call causing @Transactional failure
Spring AOP only works when a method is invoked through a proxy. Calls using this within the same class bypass the proxy, so the transaction is not applied.
<code>@RestController
@RequestMapping("/test")
public class TransactionalController {
@Resource
private OrderService orderService;
@GetMapping("testTransactional")
public String testTransactional(){
orderService.findAndInsertOrder();
return "success";
}
}
</code> <code>@Service
public class OrderService {
@Resource
private OrderMapper orderMapper;
@Transactional
public void findAndInsertOrder(){
orderMapper.getOrderById(2L);
Order order = new Order();
order.setOrderId(123456789L);
order.setNum(1);
order.setUserId(123456789L);
this.insertOrder(order); // internal call – transaction ignored
}
@Transactional
public void insertOrder(Order order){
orderMapper.insertOrder(order);
throw new RuntimeException("exception");
}
}
</code>Changing the propagation of insertOrder to Propagation.NEVER still shows that the transaction does not roll back because the method is invoked internally.
1.1 Self‑injection
Inject the service into itself so that the internal call goes through the proxy.
<code>@Service
public class OrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private OrderService orderService; // self‑injection
@Transactional(propagation = Propagation.NEVER)
public void insertOrder(Order order){
orderMapper.insertOrder(order);
}
@Transactional
public void findAndInsertOrder(){
// ...
orderService.insertOrder(order); // proxy call – transaction works
}
}
</code>1.2 Move the called method to another @Service
Placing the inner method in a separate service forces a proxy call.
<code>@Service
public class OrderService2 {
@Resource
private OrderMapper orderMapper;
@Transactional(propagation = Propagation.NEVER)
public void insertOrder(Order order){
orderMapper.insertOrder(order);
}
}
@Service
public class OrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private OrderService2 orderService2;
@Transactional
public void findAndInsertOrder(){
// ...
orderService2.insertOrder(order); // proxy call
}
}
</code>1.3 Use AopContext with exposeProxy=true
Enable @EnableAspectJAutoProxy(exposeProxy = true) and obtain the current proxy via AopContext.currentProxy() to invoke the transactional method.
<code>@Transactional
public void findAndInsertOrder(){
// ...
((OrderService) AopContext.currentProxy()).insertOrder(order);
}
</code> <code>@EnableAspectJAutoProxy(exposeProxy = true)
public class FirstApplication {
public static void main(String[] args) {
SpringApplication.run(FirstApplication.class, args);
}
}
</code>2. Non‑public method visibility
Spring only creates proxies for public methods. Declaring a transactional method as private prevents the transaction from being applied.
<code>@Transactional
private void findAndInsertOrder(){
// ...
}
</code>3. Final or static methods
Methods marked final or static cannot be overridden by proxies, so transactions are ignored.
4. Bean not managed by Spring
The class must be registered as a Spring bean using annotations such as @Component , @Service , @Controller , or @Repository . Otherwise, Spring cannot apply transaction handling.
5. Multithreaded calls
Invoking a transactional method from a new thread breaks the transactional context; the two threads use separate database connections, so rollback in one does not affect the other.
6. Database engine does not support transactions
MyISAM tables in MySQL do not support transactions, while InnoDB does. Ensure the underlying tables use a transactional engine.
<code>SHOW ENGINES;</code>7. Transaction manager not configured
If no transaction manager bean is defined, Spring cannot start transactions. Spring Boot usually auto‑configures DataSourceTransactionManager .
8. Incorrect propagation settings
Using Propagation.NEVER or other non‑supporting propagation types disables transaction creation and throws an exception if a transaction already exists.
9. Inner method uses a propagation type that disables transactions
When an inner method is annotated with Propagation.NOT_SUPPORTED or similar, it runs outside any transaction, preventing rollback.
10. Swallowing exceptions
If a transactional method catches the exception and does not rethrow it, Spring assumes the operation succeeded and will not roll back.
11. Exception type mismatch
By default, Spring rolls back only on unchecked exceptions ( RuntimeException and Error ). Checked exceptions do not trigger a rollback unless configured.
12. Misconfigured rollbackFor attribute
If rollbackFor references an exception type that is never thrown, the transaction will not roll back even when an error occurs.
By avoiding these pitfalls, developers can ensure that Spring transactions work reliably in their applications.
Lobster Programming
Sharing insights on technical analysis and exchange, making life better through technology.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.