Dealing with UnexpectedRollbackException in Spring
In Spring applications, managing transactions properly is critical to maintaining data consistency and integrity. Sometimes, developers encounter the UnexpectedRollbackException, which often indicates that an inner transaction marked for rollback causes the outer transaction to fail unexpectedly. Let us delve into understanding the Unexpected Rollback Exception in Spring and how to effectively handle it.
1. Identifying the Issue
The UnexpectedRollbackException occurs when an inner transaction or operation triggers a rollback, causing the outer transaction to also rollback unexpectedly. This usually happens in Spring when a nested transaction or a method annotated with @Transactional fails and rolls back, but the outer transaction is not aware of this and still tries to commit. As a result, Spring throws the UnexpectedRollbackException to indicate that the outer transaction cannot commit because the transaction status has been marked as rollback-only by the inner failure.
By default, Spring uses the transaction propagation behavior called Propagation.REQUIRED. This means that when a transactional method is called within another transactional context, it joins the existing transaction rather than starting a new one. If any inner method rolls back the transaction, the whole transaction is flagged rollback-only, and the outer transaction will fail to commit, leading to the UnexpectedRollbackException.
For a deeper understanding of transaction propagation and rollback behavior in Spring, you can refer to the official documentation on Spring Transaction Management.
2. Implementing Nested Transactions
Spring does not support true nested transactions unless the underlying database supports savepoints. However, you can simulate nested transaction behavior using Propagation.NESTED with a DataSource that supports savepoints. Here is an example using @Transactional(propagation = Propagation.NESTED) combined with Aspect-Oriented Programming (AOP) to separate transaction boundaries.
2.1 Spring Boot Example Using Nested Transactions
// OrderService.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Transactional
public void placeOrder() {
System.out.println("Placing order...");
// Outer transaction work here
try {
paymentService.processPayment();
} catch (Exception e) {
System.out.println("Payment failed, but continuing order processing.");
// Handle payment failure without rolling back the entire transaction
}
System.out.println("Order placed.");
}
}
@Service
public class PaymentService {
@Transactional(propagation = Propagation.NESTED)
public void processPayment() {
System.out.println("Processing payment...");
// Simulate an error
if (true) {
throw new RuntimeException("Payment service error!");
}
System.out.println("Payment processed.");
}
}
Note: While this code does not introduce a custom aspect, Spring’s transaction management uses AOP proxies under the hood. By applying @Transactional to separate service methods (OrderService and PaymentService), we naturally create distinct, AOP-driven transactional boundaries.
2.1.1 Code Explanation
This code defines two Spring services, OrderService and PaymentService. The placeOrder() method in OrderService is annotated with @Transactional, so it runs within a transaction. Inside this method, it calls processPayment() from PaymentService, which has a nested transaction due to @Transactional(propagation = Propagation.NESTED). If processPayment() throws an exception (which it always does here), the exception is caught in placeOrder(), allowing the outer transaction to continue and complete without rolling back because of the payment failure. This approach isolates the payment failure inside a nested transaction so the main order processing can succeed even if the payment fails.
2.1.2 Code Output
Placing order... Processing payment... Payment failed, but continuing order processing. Order placed.
The output shows the sequence of events during the order placement process. It starts with Placing order..., indicating the beginning of the outer transaction. Then, Processing payment... is printed when the payment process begins. Since the payment service throws an exception, the catch block handles it and prints Payment failed, but continuing order processing.. Finally, despite the payment failure, the outer transaction completes successfully, and Order placed. is printed. This demonstrates how the nested transaction allows the order to succeed even if the payment fails.
2.2 Including Propagation.REQUIRES_NEW
When a method is annotated with @Transactional(propagation = Propagation.REQUIRES_NEW), Spring suspends the existing transaction and starts a completely independent transaction.
- It does NOT use savepoints
- It does NOT participate in the outer transaction
- It commits/rolls back separately
- The outer transaction continues regardless of inner transaction outcome (unless you rethrow exceptions)
2.2.1 Difference Between NESTED and REQUIRES_NEW
| Behavior | Propagation.NESTED | Propagation.REQUIRES_NEW |
|---|---|---|
| Depends on outer transaction | Yes | No |
| Uses savepoints | Yes | No |
| Suspends outer transaction | No | Yes |
| Requires DB savepoint support | Yes | No |
| Inner rollback affects outer? | No (rollback to savepoint only) | No |
| Outer rollback affects inner commit? | Yes (because still part of same physical transaction) | No (completely separate) |
2.2.2 When to use which?
- Use NESTED – When you want child logic to be isolated but still logically part of the main transaction, and the database supports savepoints.
- Use REQUIRES_NEW – When you want fully independent execution—logging, auditing, notifications, compensating actions, retries, etc.
2.2.3 Code Example and Output
@Service
public class PaymentServiceUsingRequiresNew {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processPayment() {
System.out.println("Processing payment in a new transaction...");
throw new RuntimeException("Payment error!");
}
}
Even if the payment fails, the outer transaction will continue normally, as this failure only affects the inner independent transaction.
Placing order... Processing payment in a new transaction... Payment failed in independent transaction. Order placed.
3. Implement Sequential Transactions using TransactionTemplate
When true nested transactions aren’t supported or desired, a common pattern is to use TransactionTemplate to execute transactions sequentially. This approach explicitly starts and commits/rolls back each transaction separately, avoiding propagation surprises.
3.1 Spring Boot Example Using Sequential Transactions via TransactionTemplate
// OrderServiceWithTemplate.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
@Service
public class OrderServiceWithTemplate {
private final TransactionTemplate transactionTemplate;
private final PaymentServiceWithTemplate paymentService;
@Autowired
public OrderServiceWithTemplate(PlatformTransactionManager transactionManager, PaymentServiceWithTemplate paymentService) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
this.paymentService = paymentService;
}
public void placeOrder() {
transactionTemplate.execute(status -> {
System.out.println("Starting outer transaction - placing order...");
// Outer transaction work here
try {
paymentService.processPaymentSequentially();
} catch (Exception e) {
System.out.println("Payment failed, but outer transaction continues.");
}
System.out.println("Order placed.");
return null;
});
}
}
@Service
public class PaymentServiceWithTemplate {
private final TransactionTemplate transactionTemplate;
@Autowired
public PaymentServiceWithTemplate(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
}
public void processPaymentSequentially() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
System.out.println("Processing payment in its own transaction...");
if (true) {
throw new RuntimeException("Payment error!");
}
System.out.println("Payment processed.");
}
});
}
}
3.1.1 Code Explanation
This code uses Spring’s TransactionTemplate to manage transactions programmatically. The OrderServiceWithTemplate class defines a placeOrder() method that executes within an outer transaction using transactionTemplate.execute(). Inside this transaction, it calls processPaymentSequentially() from PaymentServiceWithTemplate, which itself starts a new transaction using another TransactionTemplate. The payment process throws a runtime exception to simulate failure. This exception is caught in placeOrder(), allowing the outer transaction to continue and complete successfully. This setup ensures that the payment transaction is independent and any failure inside it does not roll back the main order transaction, similar to nested transactions but managed programmatically.
3.1.2 Code Output
Starting outer transaction - placing order... Processing payment in its own transaction... Payment failed, but outer transaction continues. Order placed.
The output shows the flow of the order placement with programmatic transaction management. It starts with Starting outer transaction - placing order..., indicating the beginning of the main transaction. Then, Processing payment in its own transaction... is printed when the payment transaction begins. Since the payment service throws a runtime exception, the exception is caught in the outer transaction method, which prints Payment failed, but outer transaction continues.. Finally, the outer transaction completes successfully, printing Order placed.. This output demonstrates how using TransactionTemplate allows handling nested transactions manually, isolating payment failures without affecting the main order transaction.
4. Conclusion
Handling UnexpectedRollbackException effectively requires understanding Spring’s transaction propagation and isolation behavior. Using Propagation.NESTED can help create savepoint-based nested transactions where supported. Alternatively, TransactionTemplate offers explicit control over sequential transactions, making transaction boundaries clear and avoiding unexpected rollbacks. Choosing the right approach depends on your use case, database capabilities, and how much transactional isolation your business logic requires.




