Spring Boot @Transactional: Rollback and Propagation Strategies

In Java, Spring Boot
July 31, 2023

What Is a Transaction?

A “transaction” is a fundamental concept in the field of database management and refers to a sequence of one or more operations that are executed as a single unit of work. In the context of databases, a transaction ensures that a group of database operations is performed in an all-or-nothing manner, meaning that either all the operations within the transaction are completed successfully, or none of them are.

@Transacional Annotation

The @Transactional annotation is possibly the most randomly used annotation in all of Java development. The @Transactional annotation is commonly used in Spring applications to simplify transaction management and promote the “write less, do more” philosophy. It eliminates the need for manual handling of transaction boundaries and ensures that your data stays consistent and reliable in case of exceptions or failures.

@Transactional is present in Spring and JavaEE (javax.transaction package), we’ll be using the one from Spring Framework. Spring’s @Transactional annotation offers more flexibility and ease of use, making it a popular choice for transaction management in Spring applications.

Behind the @Transactional Annotation

When you see a method like this:

@Transactional
public void saveBranch() {
   // business code
}

You should keep in mind that when you call such a method, the invocation will be wrapped in transaction management code that looks like this:

UserTransaction userTransaction = entityManager.getTransaction();
try {
  // begin a new transaction if expected
  
   userTransaction.begin(); 

   saveBranch(); // the actual method invocation

   userTransaction.commit();
} catch(RuntimeException e) {
   userTransaction.rollback(); // initiate rollback if business code fails
   throw e;
}

Transactional(readOnly = true)

In Spring Framework’s @Transactional annotation, the readOnly attribute is used to indicate that a particular method or transactional operation is read-only and does not modify the underlying data. When you set readOnly = true, you inform the Spring framework that the transaction should be optimized for read-only access to the database.

Note: the readOnly parameter doesn’t guarantee its behaviour

From the documentation:

“This just serves as a hint for the actual transaction subsystem; it will not necessarily cause the failure of write access attempts. A transaction manager which cannot interpret the read-only hint will not throw an exception when asked for a read-only transaction but rather silently ignore the hint.”

Rollbacks

The rule governing automatic transaction rollbacks is both straightforward and significant: By default, any unchecked exception thrown within a transaction will prompt a rollback, while checked exceptions do not trigger rollbacks (The checked ones are treated like restorable). However, this behavior can be tailored to specific needs using two key parameters:

  1. noRollbackFor: This parameter allows customization by specifying a runtime exception that should not lead to a rollback.
  2. rollbackFor: On the other hand, the rollbackFor parameter enables the indication of which checked exception should be the trigger for rollbacks.

By leveraging these parameters, developers can finely control transaction behavior to ensure data integrity and responsiveness in their applications.

@Transactional propagation Property

This feature allows deciding whether a new transaction (similar to the highway-lane concept mentioned above) will be opened or, if there is an existing transaction, whether it will be utilized.

Propagation.REQUIRED

The default @Transactional propagation is REQUIRED. It means that the new transaction is created if it’s missing. And if it’s present already, the current one is supported. So, the whole request is being executed within a single transaction.

Example:

   @Transactional(propagation = Propagation.REQUIRED)
    public void processOrder(Order order) {
        try {
            // Deduct payment amount from customer's account
            paymentService.deductPayment(order.getCustomerId(), order.getTotalAmount());

            // Update inventory for the ordered products
            inventoryService.updateInventory(order.getItems());

            // Send order confirmation email
            emailService.sendOrderConfirmationEmail(order);
        } catch (Exception e) {
            // Handle any exceptions and possibly roll back the transaction
        
        }
    }

In above example, all of the database transactions uses the same transaction.

Propagation.SUPPORTS

If there is an existing transaction, it will use that transaction. Otherwise, it will run without a transaction and will not open a new transaction. Using Propagation.SUPPORTS allows the application to efficiently read data without the overhead of starting new transactions for read-only operations. It ensures proper data retrieval while still being able to participate in an existing transaction if one is available.

Propagation.MANDATORY

If there is no transaction exists, it throws an exception.

Propagation.REQUIRES_NEW

If there is an active transaction, it suspends it and opens a new transaction. Inner transaction uses an independent physical transaction, You should be careful and concerned about more negative scenarios, comparing to transactional flow with the default propagation mode: REQUIRED

A Good Question: Can inner transaction be committed, when outer transaction will rollback?

First of all, I want to say that Propagation.REQUIRES_NEW may not be good option for your need. It can be problematic for at least 2 reasons:

Scenario 1: Blocking resource allocation for a restricted resource (connections from a pool) already sounds like something we might have heard about in computer science when we heard about deadlocks.

Scenario 2: Handling 2 connections to the database at the same time where committing one depends on committing the other is also a bad idea — especially if the connections might operate on related tables.

In the case of Propagation.REQUIRES_NEW, the inner transaction is independent of the outer transaction. This means that if the outer transaction is rolled back, it will not affect the inner transaction. The inner transaction will continue to execute independently and can be committed, even if the outer transaction eventually rolls back.

NOTE: Actual transaction suspension will not work out-of-the-box on all transaction managers. This in particular applies to JtaTransactionManager, which requires the jakarta.transaction.TransactionManager to be made available to it (which is server-specific in standard Jakarta EE).

Propagation.NOT_SUPPORTED

If there is an existing transaction, it suspends that transaction and does not open a new transaction.

Propagation.NEVER

If there is an existing transaction, it throws an exception.

Propagation.NESTED

This parameter is used in conjunction with the savepoint mechanism developed by JDBC. If there is an existing transaction, it opens a new transaction (Nested transaction) in parallel, and while this new transaction is rolled back, the other transaction continues. If there is no transaction, it operates as “REQUIRED.” It works only with JDBC resource operations due to the use of the savepoint mechanism.

readOnly Property

When this property is set to true, a read-only transaction is opened. It can be used for transactions where no changes will be made to the database.

timeOut Property

With this property defined, if the query result does not arrive within the specified time, it performs the rollback operation.

rollbackFor Property

This property determines whether the rollback operation should be performed based on the class you have specified.

The specified class must be derived from the Throwable class.

Error classes thrown as unchecked exceptions (e.g., NullPointerException, ArrayIndexOutOfBoundsException) are not affected by this and will be rolled back by the transaction.

If the rollbackFor property needs to be used as an array, it should be used as rollbackForClassName.

Try Catch and @Transactional

When a method annotated with @Transactional is executed, the framework creates a transactional context around that method. If any runtime exception is thrown within the transactional method, the transaction will be marked for rollback. However, before the rollback is actually performed, the exception is propagated to the caller of the transactional method.

If the caller catches the exception using a try-catch block, the transaction’s rollback will not be triggered automatically. Instead, the caller can decide how to handle the exception. If the caller chooses not to rethrow the exception or perform any action that would cause the transaction to rollback, the transaction will be committed when the transactional method completes normally.

Example:

@Service
public class MyService {

    @Autowired
    private MyRepository myRepository;

    @Transactional
    public void transactionalMethod() {
        try {
            // Some database operations or business logic that may throw an exception
            myRepository.doSomething();
        } catch (Exception e) {
            // Catching the exception, but not rethrowing it, will not trigger rollback
            // Transaction will be committed if there is no other action that causes a rollback.
        }
    }
}