Spring @Transactional Rollback Handling with Code Explanation
Bydefault, Spring Boot transactions are auto-committed. This means that every single SQL statement is treated as its transaction and will commit after execution. Here’s an example that illustrates this behaviour:
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();
}
In this example, the product is inserted into the database even though an exception has been raised. This default behaviour might not be suitable for scenarios where multiple transactions are run in a logical unit of code.
In Spring Boot, when @Transactional annotation is used, Spring Boot implicitly creates a proxy that will be creating a connection to the database. A transaction will be started and committed after the code has been executed errorless. Otherwise, it will roll back the changes if an exception occurred.
import java.sql.Connection;
Connection conn = dataSource.getConnection();
try (connection) {
// execute some SQL statements...
// commit transaction
conn.commit();
} catch (SQLException e) {
conn.rollback();
}
Example 1: Basic Usage of @Transactional
The @Transactional
annotation in Spring Boot implicitly creates a proxy that starts a transaction and commits it if no errors occur. If an exception occurs, it rolls back the changes.
@Transactional
public void createProduct() {
Product prod = new Product();
prod.setDescription("This is an example with runtime exception and transactional annotation.");
prod.setPrice(10);
prod.setTitle("Second Product");
productRepository.save(prod);
throw new RuntimeException();
}
The @Transactional
annotation ensures that the transaction is rolled back if an exception occurs. Note that Spring only rolls back on unchecked exceptions
by default.
Example 2a: Checked Exception Without Rollback
In this example, the checked exception will not be rolled back even though the @Transactional
annotation is used. This is because SQLException is a checked exceptions
.
@Transactional
public void createProduct() throws Exception {
Product prod = new Product();
prod.setDescription("This is an example with checked exception and transactional annotation.");
prod.setPrice(10);
prod.setTitle("Second Product");
productRepository.save(prod);
throw new SQLException();
}
Example 2b: Using rollbackFor
to Roll Back for Specific Exceptions
To roll back checked exceptions, the rollbackFor
attribute can be specified.
@Transactional(rollbackFor = SQLException.class)
public void createProduct() throws Exception {
Product prod = new Product();
prod.setDescription("This is an example with checked exception and transactional annotation with rollbackFor.");
prod.setPrice(10);
prod.setTitle("Example 2b Product");
productRepository.save(prod);
throw new SQLException();
}
We see that the transaction has rolled back successfully.
Example 2c: Using noRollbackFor
to Prevent Rollback for Specific Exceptions
Conversely, you may want to roll back for all exceptions except specific ones. You can use the noRollbackFor
attribute to achieve this.
@Transactional(noRollbackFor = NonCriticalException.class)
public void processPayment() throws Exception {
// Some code that may throw NonCriticalException or CriticalException
if (someCondition) {
throw new NonCriticalException("Don't roll back for this exception!");
} else {
throw new CriticalException("Roll back this transaction!");
}
}
In this example, if NonCriticalException
is thrown, the transaction will not be rolled back. If any other exception (RuntimeException), such as CriticalException
, is thrown, the transaction will be rolled back.
Example 3: Using Try-Catch Block Inside @Transactional
In this example, we’ll explore what happens when a try-catch block is used inside a method annotated with @Transactional
.
@Transactional
public void createProduct() {
try {
System.out.println("------ createProduct ------");
Product prod = new Product();
prod.setDescription("This is an example with runtime exception, transactional annotation and try catch.");
prod.setPrice(10);
prod.setTitle("Example 3 Product");
productRepository.save(prod);
System.out.println("Example 3 Product inserted.");
throw new RuntimeException();
} catch (Exception e) {
System.out.println("Here we catch the exception.");
}
}
In this scenario, the try-catch block inside the method catches the RuntimeException. Since the exception is caught and handled within the method, the transaction is not rolled back. Instead, it’s executed normally and committed.
Example 4: Nested Transactions with Rollback
Spring transactions can be nested, and the behavior of nested transactions can be controlled using propagation settings.
In this example, both createProduct()
and createOrder()
methods are annotated with @Transactional
. The createProduct()
method calls the createOrder()
method, and a RuntimeException is thrown inside createOrder()
.
//ProductController.java
@Transactional
public void createProduct() {
Product prod = new Product();
prod.setDescription("This is createProduct method.");
prod.setPrice(10);
prod.setTitle("Create Product");
productRepository.save(prod);
orderController.createOrder();
}
//OrderController.java
@Transactional
public void createOrder() {
Order order = new Order();
order.setTitle("Create Order");
order.setDescription("This is createOrder method with runtime exception");
orderRepository.save(order);
throw new RuntimeException("Create Order RuntimeException");
}
In this example, both createProduct()
and createOrder()
transactions are rolled back if a runtime exception occurs in the createOrder()
method.
Propagation Behavior: By default, the
@Transactional
annotation uses a propagation behavior calledPropagation.REQUIRED
. This means that if there is an active transaction, Spring will join that transaction instead of creating a new one. In this example, whencreateOrder()
is called fromcreateProduct()
, it joins the existing transaction started bycreateProduct()
.Exception Handling: Since a RuntimeException is thrown inside
createOrder()
, and it's not caught within the method, the exception propagates to the calling method (createProduct()
). Since the exception is not handled, the entire transaction is marked for rollback.Single Logical Transaction: Because both methods are part of the same logical transaction (due to the default propagation behavior), if one part of the transaction fails, the entire transaction is rolled back. This ensures the atomicity of the transaction, meaning that all changes are either committed or rolled back together.
Example 5: Handling RuntimeException with Try-Catch in Nested Transactions
In this example, the createProduct()
method calls the createOrder()
method. Inside createOrder()
, a RuntimeException is thrown. The createProduct()
method catches this exception using a try-and-catch block.
//ProductController.java
@Transactional
public void createProduct() {
System.out.println("------ createProduct ------");
Product prod = new Product();
prod.setDescription("This is createProduct method.");
prod.setPrice(10);
prod.setTitle("Create Product");
productRepository.save(prod);
try {
orderController.createOrder();
} catch (RuntimeException e) {
System.out.println("Handle " + e.getMessage());
}
}
//OrderController.java
@Transactional
public void createOrder() {
System.out.println("------ createOrder ------");
Order order = new Order();
order.setTitle("Create Order");
order.setDescription("This is createOrder method with runtime exception");
orderRepository.save(order);
throw new RuntimeException("Create Order RuntimeException");
}
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
Why Are Both Records Rolled Back?
Propagation Behavior: As in the previous examples, the default propagation behavior is
Propagation.REQUIRED
. This means that both methods are part of the same logical transaction. Spring will start and end the transaction according to the propagation settingException Handling: Even though the
createProduct()
method catches the RuntimeException, the transaction is already marked for rollback by the time the exception is caught. This is because the@Transactional
annotation works at the proxy level, and the rollback decision is made before the catch block is executed.Transaction Silently Rolled Back: The exception
org.springframework.transaction.UnexpectedRollbackException
indicates that the transaction was silently rolled back because it was marked as rollback-only. This means that something within the transaction boundary has called for a rollback, and the transaction system is enforcing that decision.Atomicity: The behaviour ensures the atomicity of the transaction. If one part of the transaction fails, the entire transaction is rolled back, maintaining the consistency of the system.
Example 6: Using Propagation.REQUIRES_NEW
Using Propagation.REQUIRES_NEW
forces Spring to create a subtransaction, allowing the outer transaction to commit even if the inner transaction rolls back.
@Transactional
public void createProduct() {
Product prod = new Product();
prod.setDescription("This is createProduct method.");
prod.setPrice(10);
prod.setTitle("Create Product");
productRepository.save(prod);
try {
orderController.createOrder();
} catch (RuntimeException e) {
System.out.println("Handle " + e.getMessage());
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrder() {
Order order = new Order();
order.setTitle("Create Order");
order.setDescription("This is createOrder method with runtime exception");
orderRepository.save(order);
throw new RuntimeException("Create Order RuntimeException");
}
The product record was written into the database, and the order record was rolled back.
Example 7: Exception in Inner Method with Propagation.REQUIRES_NEW
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?
@Transactional
public void createProduct() {
System.out.println("------ createProduct ------");
Product prod = new Product();
prod.setDescription("This is createProduct method.");
prod.setPrice(10);
prod.setTitle("Create Product");
productRepository.save(prod);
orderController.createOrder();
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void createOrder() {
System.out.println("------ createOrder ------");
Order order = new Order();
order.setTitle("Create Order");
order.setDescription("This is createOrder method with runtime exception");
orderRepository.save(order);
throw new RuntimeException("Create Order RuntimeException");
}
In this example, the createOrder()
method is annotated with Propagation.REQUIRES_NEW
, which means it will start a new transaction separate from the existing one. However, when an exception is thrown in the inner method (createOrder()
), it is not caught, and both transactions are rolled back.
New Transaction: The
Propagation.REQUIRES_NEW
setting starts a new transaction forcreateOrder()
, but since the exception is not handled, it marks the transaction for rollback.Outer Transaction Affected: The unhandled exception in the inner transaction also affects the outer transaction (
createProduct()
), causing it to be rolled back as well.
Example 8: Exception in Outer Method with Propagation.REQUIRES_NEW
In this example, the exception is thrown in the outer method (createProduct()
), after the inner method (createOrder()
) has completed successfully.
@Transactional
public void createProduct() {
System.out.println("------ createProduct ------");
Product prod = new Product();
prod.setDescription("This is createProduct method.");
prod.setPrice(10);
prod.setTitle("Create Product with runtime");
productRepository.save(prod);
orderController.createOrder();
throw new RuntimeException("Create Product RuntimeException");
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void createOrder() {
System.out.println("------ createOrder ------");
Order order = new Order();
order.setTitle("Create Order with propagation required_new");
order.setDescription("This is createOrder method.");
orderRepository.save(order);
}
New Transaction for Inner Method: The
Propagation.REQUIRES_NEW
setting starts a new transaction forcreateOrder()
, and since no exception is thrown in this method, the transaction is committed successfully.Exception in Outer Method: The exception in
createProduct()
causes the outer transaction to be rolled back, but it does not affect the inner transaction, which has already been committed.
Additional Notes
- However, it’s essential to note that if both methods are in the same class, the
@Transactional
annotation will not create a new transaction, even withPropagation.REQUIRES_NEW
. This is because the internal method call will bypass the proxy created by Spring, and the propagation setting will not take effect.
Thanks for reading! If you have any more questions, feel free to ask. Cheers! 🍸
Subscribe to my newsletter
Read articles from Wynn Teo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Wynn Teo
Wynn Teo
Passionate full-stack and AWS-certified engineer with 9+ years of hands-on experience developing websites and applications.