Understanding Transaction rolled back because it has been marked as rollback-only in Spring
Spring transactions are powerful but sometimes tricky. Here's a real-world debugging story about a rollback-only error I ran into — and how to fix it. Recently, while working on a project, I encountered the following exception: org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only This error is related to Spring's transaction management. Why Does This Happen? In a nested transaction scenario, when using the default transaction propagation behavior, if an inner transaction throws an exception, but the outer transaction catches it and continues executing normally, Spring will raise a rollback-only error. Here's why: In Spring, transactions are managed via AOP (Aspect-Oriented Programming). When a method managed by a transaction finishes without exception, Spring commits the SQL operations. If an exception is thrown, Spring rolls back the transaction. By default, Spring uses PROPAGATION_REQUIRED propagation: If there is no existing transaction, it creates a new one. If there is already an existing transaction, it joins it. In most projects, we use the default propagation. That way, if either the inner or outer method fails, the entire transaction is rolled back. However, in a nested transaction scenario: When the inner method throws an exception e, Spring marks the whole transaction as rollback-only. If the outer method catches the exception and continues executing normally, Spring gets confused: it sees the method completed "normally", but the transaction was already marked for rollback. At this point, it throws: org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only Example Code Here’s a minimal reproduction: @Service public class OrderService { @Autowired private PaymentService paymentService; @Transactional public boolean createOrder() { try { // Some business logic... paymentService.processPayment(); // Inner transaction // More business logic... return true; } catch (Exception e) { // Outer method catches exception but doesn't rethrow return false; } } } @Service public class PaymentService { @Transactional public void processPayment() { // Simulate an error during payment processing throw new RuntimeException("Payment failed!"); } } In this example: processPayment() throws an exception. createOrder() catches the exception and returns false without rethrowing. Spring detects the transaction was marked as rollback-only but the method finished normally, and throws UnexpectedRollbackException. How to Solve It? Depending on the desired behavior, there are several solutions: Force the program to stop execution immediately after the inner exception: In the outer transaction’s catch block, rethrow the exception. try { paymentService.processPayment(); } catch (Exception e) { throw e; // Important: rethrow! } Handle the exception within the inner transaction: Catch exceptions inside the inner service so that the outer transaction is not affected. @Transactional public void processPayment() { try { // Business logic } catch (Exception ex) { // Handle exception internally } } Use PROPAGATION_NESTED propagation for the inner transaction: If you want the inner transaction to rollback without affecting the outer transaction, use PROPAGATION_NESTED. @Transactional(propagation = Propagation.NESTED) public void processPayment() { throw new RuntimeException("Payment failed!"); } PROPAGATION_NESTED creates a savepoint. If the inner method fails, it rolls back to the savepoint, and the outer method can continue normally. It's important that the database supports savepoints. Root Cause in My Project In my project, the root cause of the rollback-only error was inconsistent error handling styles between inner and outer transactions: Outer methods used return true/false to signal success or failure. Inner methods used exceptions to signal errors. This mismatch led to the UnexpectedRollbackException. Even though the final SQL changes were correctly rolled back, the calling method expected true/false, but instead received an exception, leading to confusion. Quick Reference: Spring Transaction Propagation Types Propagation Type Description REQUIRED (default) Join existing transaction, or create a new one if none exists. REQUIRES_NEW Always create a new transaction, suspending the existing one. NESTED Create a nested transaction using savepoints. SUPPORTS Execute within a transaction if one exists; otherwise, non-transactional. NOT_SUPPORTED Suspend any existing transaction. MANDATORY Must

Spring transactions are powerful but sometimes tricky. Here's a real-world debugging story about a rollback-only error I ran into — and how to fix it.
Recently, while working on a project, I encountered the following exception:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
This error is related to Spring's transaction management.
Why Does This Happen?
In a nested transaction scenario, when using the default transaction propagation behavior, if an inner transaction throws an exception, but the outer transaction catches it and continues executing normally, Spring will raise a rollback-only
error.
Here's why:
- In Spring, transactions are managed via AOP (Aspect-Oriented Programming).
- When a method managed by a transaction finishes without exception, Spring commits the SQL operations.
- If an exception is thrown, Spring rolls back the transaction.
- By default, Spring uses
PROPAGATION_REQUIRED
propagation:- If there is no existing transaction, it creates a new one.
- If there is already an existing transaction, it joins it.
In most projects, we use the default propagation. That way, if either the inner or outer method fails, the entire transaction is rolled back.
However, in a nested transaction scenario:
- When the inner method throws an exception
e
, Spring marks the whole transaction as rollback-only. - If the outer method catches the exception and continues executing normally, Spring gets confused: it sees the method completed "normally", but the transaction was already marked for rollback.
- At this point, it throws:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
Example Code
Here’s a minimal reproduction:
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Transactional
public boolean createOrder() {
try {
// Some business logic...
paymentService.processPayment(); // Inner transaction
// More business logic...
return true;
} catch (Exception e) {
// Outer method catches exception but doesn't rethrow
return false;
}
}
}
@Service
public class PaymentService {
@Transactional
public void processPayment() {
// Simulate an error during payment processing
throw new RuntimeException("Payment failed!");
}
}
In this example:
-
processPayment()
throws an exception. -
createOrder()
catches the exception and returnsfalse
without rethrowing. - Spring detects the transaction was marked as rollback-only but the method finished normally, and throws
UnexpectedRollbackException
.
How to Solve It?
Depending on the desired behavior, there are several solutions:
- Force the program to stop execution immediately after the inner exception:
In the outer transaction’s catch
block, rethrow the exception.
try {
paymentService.processPayment();
} catch (Exception e) {
throw e; // Important: rethrow!
}
- Handle the exception within the inner transaction:
Catch exceptions inside the inner service so that the outer transaction is not affected.
@Transactional
public void processPayment() {
try {
// Business logic
} catch (Exception ex) {
// Handle exception internally
}
}
- Use
PROPAGATION_NESTED
propagation for the inner transaction:
If you want the inner transaction to rollback without affecting the outer transaction, use PROPAGATION_NESTED
.
@Transactional(propagation = Propagation.NESTED)
public void processPayment() {
throw new RuntimeException("Payment failed!");
}
PROPAGATION_NESTED
creates a savepoint.
If the inner method fails, it rolls back to the savepoint, and the outer method can continue normally.
It's important that the database supports savepoints.
Root Cause in My Project
In my project, the root cause of the rollback-only
error was inconsistent error handling styles between inner and outer transactions:
- Outer methods used
return true/false
to signal success or failure. - Inner methods used exceptions to signal errors.
This mismatch led to the UnexpectedRollbackException
.
Even though the final SQL changes were correctly rolled back, the calling method expected true/false, but instead received an exception, leading to confusion.
Quick Reference: Spring Transaction Propagation Types
Propagation Type | Description |
---|---|
REQUIRED (default) |
Join existing transaction, or create a new one if none exists. |
REQUIRES_NEW |
Always create a new transaction, suspending the existing one. |
NESTED |
Create a nested transaction using savepoints. |
SUPPORTS |
Execute within a transaction if one exists; otherwise, non-transactional. |
NOT_SUPPORTED |
Suspend any existing transaction. |
MANDATORY |
Must execute within an existing transaction; otherwise, throw an exception. |
NEVER |
Must execute without a transaction; otherwise, throw an exception. |
Hope this helps if you ever encounter a similar issue!