Spring Transaction Management Rollback 2025
Contents
Understanding Spring Transaction Management
By default, without any transaction management, Spring operates in auto-commit mode. Every single SQL statement executes in its own transaction and commits immediately after execution. This behavior can lead to data inconsistency issues when multiple related operations need to be treated as a single atomic unit.
The Problem with Auto-Commit
Consider this basic example without spring transaction management:
public void createProduct() {
Product prod = new Product();
prod.setDescription("This is an example with runtime exception but no rollback.");
prod.setPrice(10);
prod.setTitle("First Product");
productRepository.save(prod);
throw new RuntimeException();
}
Problem: The product gets inserted into the database even though an exception occurs afterward.
How @Transactional Annotation Works
The @Transactional
annotation in Spring implicitly creates a proxy that initiates a transaction and commits it if no errors occur. When an exception arises, it ensures that the changes are rolled back. In this example, the transaction is rolled back when a RuntimeException
occurs, maintaining data consistency:
// What Spring does internally:
Connection conn = dataSource.getConnection();
try (connection) {
// Execute your business logic
conn.commit();
} catch (Exception e) {
conn.rollback();
}
What happens behind the scenes:
- Spring creates a transaction proxy around your method
- A database connection is obtained and auto-commit is disabled
- Your business logic executes within the transaction boundary
- When RuntimeException is thrown, Spring catches it and calls
connection.rollback()
- All database changes within the transaction are undone
1. RuntimeException Triggers Automatic Rollback
When a RuntimeException
is thrown within a transactional method, Spring automatically rolls back all database changes made within that transaction boundary:
@Service
public class ProductService {
@Transactional
public void createProduct() {
Product product = new Product();
product.setTitle("Test Product");
product.setPrice(100);
productRepository.save(product);
throw new RuntimeException("Business validation failed");
}
}
Product is NOT saved to database. Spring automatically rolls back the transaction when RuntimeException
is thrown, undoing all database operations within the transaction boundary.
2. Checked Exceptions Don’t Rollback by Default
Spring treats checked exceptions as expected business scenarios that shouldn’t invalidate the entire transaction. Only unchecked exceptions trigger automatic rollback.
@Service
public class ProductService {
@Transactional
public void createProduct() throws SQLException {
Product product = new Product();
product.setTitle("Test Product");
product.setPrice(100);
productRepository.save(product);
throw new SQLException("Connection timeout");
}
}
Product is saved to database. Spring does not rollback on checked exceptions by default. The SQLException
is propagated to the caller, but the transaction commits first.
3. Force Rollback on Checked Exceptions
However, we can explicitly specify which exceptions should trigger rollback using the rollbackFor
attribute:
@Service
public class ProductService {
@Transactional(rollbackFor = SQLException.class)
public void createProduct() throws SQLException {
Product product = new Product();
product.setTitle("Test Product");
product.setPrice(100);
productRepository.save(product);
throw new SQLException("Connection timeout");
}
}
In this case, Product is NOT saved to database. The rollbackFor
attribute explicitly tells Spring to rollback when SQLException occurs, overriding the default behavior for checked exceptions.
4. Force No Rollback on Unchecked Exceptions
Conversely, we may want to roll back for all unchecked exceptions except specific ones. Use the noRollbackFor
attribute to achieve this:
@Service
public class ProductService {
@Transactional(noRollbackFor = RuntimeException.class)
public void createProduct() {
Product product = new Product();
product.setTitle("Test Product");
product.setPrice(100);
productRepository.save(product);
throw new RuntimeException("Business validation failed");
}
}
In this example, the product is saved to database.
Exception Types and Rollback Behavior
When we catch and handle exceptions within transactional methods, Spring never sees the exception and treats the method as completing successfully, preventing automatic rollback.
1. Try-Catch Blocks Prevent Rollback
Exception handling can accidentally prevent necessary rollbacks when exceptions are caught and not re-thrown:
@Service
public class ProductService {
@Transactional
public void createProduct() {
try {
Product product = new Product();
product.setTitle("Test Product");
product.setPrice(100);
productRepository.save(product);
performRiskyOperation();
} catch (Exception e) {
logger.error("Exception caught: " + e.getMessage());
}
}
private void performRiskyOperation() {
throw new RuntimeException("Critical failure");
}
}
In this case, the product is saved to database. The exception is caught and handled, so Spring never sees the exception. From Spring’s perspective, the method completed successfully, so the transaction commits.
2. Manual Rollback Control
We can manually mark a transaction for rollback even when handling exceptions using setRollbackOnly()
.
@Service
public class ProductService {
@Transactional
public void createProductWithManualRollback() {
try {
Product product = new Product();
product.setTitle("Test Product");
product.setPrice(100);
productRepository.save(product);
performRiskyOperation();
} catch (Exception e) {
logger.error("Exception occurred: " + e.getMessage());
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw e;
}
}
}
Product is NOT saved to database. Even though the exception is caught, setRollbackOnly()
manually marks the transaction for rollback. The re-thrown exception ensures the rollback is triggered.
3. Custom Rollback Rules
We can define specific exceptions that should or should not trigger rollback using rollbackFor
and noRollbackFor
attributes:
@Service
public class ProductService {
@Transactional(
rollbackFor = {SQLException.class, CustomBusinessException.class},
noRollbackFor = {DataIntegrityViolationException.class}
)
public void createProductWithCustomRules() throws SQLException {
Product product = new Product();
product.setTitle("Custom Rollback Rules");
product.setPrice(100);
productRepository.save(product);
if (someBusinessCondition()) {
throw new SQLException("Database error");
}
}
}
Behavior depends on which exception is thrown. SQLException
and CustomBusinessException
will trigger rollback (product NOT saved), while DataIntegrityViolationException
will not trigger rollback (product is saved).
Transaction Propagation and Nested Rollbacks
Transaction propagation defines how transactions behave when one transactional method calls another transactional method. By default, the @Transactional
annotation uses a propagation behavior called Propagation.REQUIRED
. This means that if there is an active transaction, Spring will join that transaction instead of creating a new one.
1. Nested Transaction
With default REQUIRED propagation, all methods share the same transaction context, so any failure affects the entire transaction.
@Service
public class ProductService {
@Autowired
private OrderService orderService;
@Transactional
public void createProduct() {
Product product = new Product();
product.setTitle("Test Product");
product.setPrice(100);
productRepository.save(product);
orderService.createOrder();
}
}
@Service
public class OrderService {
@Transactional
public void createOrder() {
Order order = new Order();
order.setTitle("Test Order");
orderRepository.save(order);
throw new RuntimeException("Order processing failed");
}
}
Both Product and Order are NOT saved to database. With REQUIRED
propagation (default), both methods share the same transaction.
Since a RuntimeException
is thrown inside createOrder()
, and it’s not caught within the method, the exception propagates to the calling method createProduct()
. The exception is not handled by outer method as well, so the entire transaction is marked as rollback-only.
This ensures the atomicity of the transaction, meaning that all changes are either committed or rolled back together.
2. Exception Handling in Nested Transactions
Let’s add in the try-and-catch block to handle the runtime exception in the createProduct() method.
@Service
public class ProductService {
@Autowired
private OrderService orderService;
@Transactional
public void createProduct() {
Product product = new Product();
product.setTitle("Test Product");
product.setPrice(100);
productRepository.save(product);
try {
orderService.createOrder();
} catch (RuntimeException e) {
logger.error("Order creation failed: " + e.getMessage());
}
}
}
Arg? Both records have been rollback. Why? At the same time, we also noticed the exception error.
org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
Even though the exception is caught, the shared transaction is already marked as rollback-only by the inner method. Spring throws UnexpectedRollbackException
because rollback-only transactions cannot commit.
3. Independent Transactions (REQUIRES_NEW) with Exception Handling
Using REQUIRES_NEW
propagation creates independent transactions that can commit or rollback separately:
@Service
public class ProductService {
@Autowired
private OrderService orderService;
@Transactional
public void createProduct() {
Product product = new Product();
product.setTitle("Test Product");
product.setPrice(100);
productRepository.save(product);
try {
orderService.createOrder();
} catch (RuntimeException e) {
logger.error("Order creation failed: " + e.getMessage());
}
}
}
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrder() {
Order order = new Order();
order.setTitle("Independent Order");
orderRepository.save(order);
throw new RuntimeException("Order processing failed");
}
}
Using Propagation.REQUIRES_NEW
forces Spring to create a subtransaction, allowing the outer transaction to commit even if the inner transaction rolls back. In this example, the product is saved into the database, and the order record is rolled back.
4. Independent Transactions Without Exception Handling
What if we never use try and catch block in the outer method, and an exception has happened in the inner method? Will the outer transaction still commit?
@Service
public class ProductService {
@Autowired
private OrderService orderService;
@Transactional
public void createProduct() {
Product product = new Product();
product.setTitle("Test Product");
product.setPrice(100);
productRepository.save(product);
orderService.createOrder();
}
}
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrder() {
Order order = new Order();
order.setTitle("Independent Order");
orderRepository.save(order);
throw new RuntimeException("Order processing failed");
}
}
We see that both transactions have been rolled back. Because the inner transaction threw an exception, the outer transaction detected the exception and it hasn’t been handled. Therefore, the outer transaction has been rolled back.
5. Outer Transaction Failure with Independent Inner Transaction
When the inner transaction uses REQUIRES_NEW
and completes successfully, its changes remain committed even if the outer transaction fails:
@Service
public class ProductService {
@Autowired
private OrderService orderService;
@Transactional
public void createProduct() {
Product product = new Product();
product.setTitle("Test Product");
product.setPrice(100);
productRepository.save(product);
orderService.createOrder();
throw new RuntimeException("Product processing failed");
}
}
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrder() {
Order order = new Order();
order.setTitle("Independent Order");
orderRepository.save(order);
}
}
In this example, the order is saved, product is NOT saved. The inner transaction with REQUIRES_NEW
completes and commits independently first. When the outer transaction fails, only the outer transaction rolls back while the inner transaction’s changes remain permanent.
Common Pitfalls and Solutions
Understanding common pitfalls helps avoid transaction-related bugs in production applications.
Self-Invocation Problem
Internal method calls bypass Spring’s transaction proxy, causing @transactional
annotations to be ignored:
@Service
public class ProductService {
@Autowired
private ApplicationContext applicationContext;
@Transactional
public void createMultipleProducts() {
ProductService self = applicationContext.getBean(ProductService.class);
self.createProduct1();
self.createProduct2();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createProduct1() {
productRepository.save(new Product("Product 1"));
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createProduct2() {
productRepository.save(new Product("Product 2"));
throw new RuntimeException("Should rollback");
}
}
It’s essential to note that if both methods are in the same class, the @Transactional
annotation will not create a new transaction, even with Propagation.REQUIRES_NEW
.
This is because the internal method call will bypass the proxy created by Spring, and the propagation setting will not take effect.
Private Method Limitations
The @Transactional annotations only work on public methods due to proxy limitations.
@Service
public class ProductService {
@Transactional
public void createProductPublic() {
Product product = new Product("Public Method");
productRepository.save(product);
throw new RuntimeException("Will rollback correctly");
}
@Transactional
private void createProductPrivate() {
Product product = new Product("Private Method");
productRepository.save(product);
throw new RuntimeException("Annotation ignored - won't rollback!");
}
}
Async Method Issues
Asynchronous methods run in different threads, causing transaction context to be lost:
@Service
public class AsyncProductService {
@Autowired
private TransactionTemplate transactionTemplate;
@Async
public CompletableFuture<Product> createProductAsync() {
return CompletableFuture.supplyAsync(() -> {
return transactionTemplate.execute(status -> {
try {
Product product = new Product("Async Product");
productRepository.save(product);
if (product.getPrice() < 0) {
throw new RuntimeException("Will rollback correctly");
}
return product;
} catch (Exception e) {
status.setRollbackOnly();
throw e;
}
});
});
}
}
Conclusion
In this tutorial, we explored the fundamental concepts of transactions in Spring and how to rollback spring transaction on checked exception. We began by understanding the basic usage of the @Transactional
annotation, which automatically starts and commits transactions, rolling them back when exceptions occur. We dive into the differentiation between checked and unchecked exceptions, exploring how to control rollback behavior using attributes like rollbackFor
and noRollbackFor
.
In the later part, we examined the interaction between transactions and try-catch blocks. Furthermore, we explored the use of Propagation.REQUIRES_NEW
to create new transactions and how the behavior of inner and outer transactions can be controlled independently.