Spring Boot - @Transactional

@Transactional annotation is used to define the scope of a single database transaction. If there is an exception thrown in the method, the transaction is rolled back. If the method completes successfully, the transaction is committed.

@Transactional

When used on a class, it applies to all the public methods in the class. When used on a method, it applies only to that method.

The default @Transactional settings are as follows:

  • The propagation setting is PROPAGATION_REQUIRED.
  • The isolation level is ISOLATION_DEFAULT.
  • The transaction is read-write.
  • The transaction timeout defaults to the default timeout of the underlying transaction system, or to none if timeouts are not supported.
  • Any RuntimeException or Error triggers rollback, and any checked Exception does not.
1
2
3
4
@Transactional
public void saveCustomer() {
customerRepository.save(new Customer("Alice", "Smith"));
}

If the saveCustomer method throws an exception, the transaction is rolled back. If the method completes successfully, the transaction is committed.

Propagation

The propagation setting is used to define the transaction boundaries. The following are the propagation settings:

  • REQUIRED - If a transaction exists, use it. If no transaction exists, create a new one.
  • SUPPORTS - If a transaction exists, use it. If no transaction exists, run without a transaction.
  • MANDATORY - If a transaction exists, use it. If no transaction exists, throw an exception.
  • REQUIRES_NEW - Always create a new transaction.
  • NOT_SUPPORTED - Run without a transaction. If a transaction exists, suspend it.
  • NEVER - Run without a transaction. If a transaction exists, throw an exception.
  • NESTED - If a transaction exists, create a nested transaction. If no transaction exists, create a new transaction.

default propagation is REQUIRED.

setting propagation to REQUIRES_NEW

1
2
3
4
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveCustomer() {
customerRepository.save(new Customer("Alice", "Smith"));
}

REQUIRES_NEW

Setting propagation = Propagation.REQUIRES_NEW indicates that the annotated method should always execute in a new transaction. If there is an existing transaction, it will be suspended and a new transaction will be started. Once the new transaction completes, the previous transaction will resume.

You should use REQUIRES_NEW in the following scenarios:

Independent Operations: When you need the operations in the annotated method to be independent of the calling transaction. For instance, if the method needs to commit its changes regardless of the outer transaction’s outcome.

Audit Logging: When logging actions need to be recorded even if the main transaction fails. This ensures that logs are written independently of the main transaction’s success or failure.

Error Handling: When you need to ensure certain operations are completed even if the surrounding transaction is rolled back. For example, sending an email notification or updating a status flag.

Isolation: When the method’s changes should not affect or be affected by the outer transaction. This can be important in situations where you want to avoid transaction propagation and ensure strict isolation of database operations.

Here is an example of using @Transactional(propagation = Propagation.REQUIRES_NEW) in a Spring Boot application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class UserService {

@Transactional
public void updateUser(User user) {
// Update user details
updateUserDetails(user);

// Log the update operation in a separate transaction
logUserUpdate(user);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logUserUpdate(User user) {
// Log the update operation
// This will run in a separate transaction
}
}

Important considerations when using REQUIRES_NEW:

Performance Overhead:
Creating a new transaction has a performance overhead, so use it judiciously.

Connection Pool Exhaustion:
If overused, it can lead to connection pool exhaustion as each new transaction requires its own connection

NESTED

Setting propagation = Propagation.NESTED indicates that the annotated method should execute in a nested transaction. If there is an existing transaction, the method will create a savepoint within that transaction. If there is no existing transaction, a new transaction will be started.

NESTED propagation is useful in scenarios where you want to create a nested transaction within an existing transaction. This allows you to have a savepoint within the outer transaction, so that if the nested transaction fails, you can roll back only the changes made within the nested transaction, while still keeping the changes made in the outer transaction intact. This can be helpful in complex business logic scenarios where you need to ensure atomicity and consistency of multiple related operations within a larger transaction. An example use case for NESTED propagation is when you need to perform a series of database updates, and if any of the updates fail, you want to roll back only the changes made in that specific update, while still keeping the changes made in the previous updates.

NOT_SUPPORTED

Setting propagation = Propagation.NOT_SUPPORTED indicates that the annotated method should execute without a transaction. If there is an existing transaction, it will be suspended for the duration of the method execution.

NOT_SUPPORTED propagation is useful in scenarios where you want to run a method outside the scope of any transaction. This can be helpful when you want to perform read-only operations that do not require transactional support, or when you want to run a method that should not be affected by the current transaction context. An example use case for NOT_SUPPORTED propagation is when you need to perform a series of read-only database queries that do not require transactional support, or when you want to run a method that should not be affected by the current transaction context.

NEVER

Setting propagation = Propagation.NEVER indicates that the annotated method should execute without a transaction. If there is an existing transaction, an exception will be thrown.

Use Case:
Use Propagation.NEVER when you want to guarantee that specific pieces of code are not executed within a transaction context. This can be useful when you want to ensure that certain operations are always executed outside the scope of any transaction, or when you want to prevent specific methods from being called within a transaction context. If a method annotated with Propagation.NEVER is called within a transaction context, an IllegalTransactionStateException will be thrown, preventing the method from executing.

Alternatives is to use Propogation.NOT_SUPPORTED, which will suspend the current transaction if one exists, and execute the method without a transaction. If no transaction exists, the method will be executed without a transaction. Propagation.NOT_SUPPORTED is more flexible than Propagation.NEVER, as it allows the method to be executed both within and outside a transaction context, while Propagation.NEVER strictly prohibits the method from being executed within a transaction context.

Isolation

The isolation level determines how much a transaction is isolated from other transactions. The following are the isolation levels:

  • DEFAULT
  • READ_UNCOMMITTED
  • READ_COMMITTED
  • REPEATABLE_READ
  • SERIALIZABLE

default isolation level is DEFAULT.

setting isolation level to READ_COMMITTED

1
2
3
4
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void saveCustomer() {
customerRepository.save(new Customer("Alice", "Smith"));
}

Read-Only

The read-only setting is used to specify whether the transaction is read-only. If the transaction is read-only, the transaction is optimized for read operations. The default setting is false.

setting read-only to true

1
2
3
4
@Transactional(readOnly = true)
public void saveCustomer() {
customerRepository.save(new Customer("Alice", "Smith"));
}

rollbackFor

The rollbackFor setting is used to specify which exceptions trigger a rollback. By default, any RuntimeException or Error triggers a rollback. Any checked Exception does not trigger a rollback.

setting rollbackFor to IllegalArgumentException

1
2
3
4
@Transactional(rollbackFor = IllegalArgumentException.class)
public void saveCustomer() {
customerRepository.save(new Customer("Alice", "Smith"));
}

transactionManagers

The transactionManager setting is used to specify the transaction manager to use. If not specified, the default transaction manager is used.

setting transactionManager to transactionManager2

1
2
3
4
@Transactional(transactionManager = "transactionManager2")
public void saveCustomer() {
customerRepository.save(new Customer("Alice", "Smith"));
}

Most Spring applications need only a single transaction manager, but there may be situations where you want multiple independent transaction managers in a single application. You can use the value or transactionManager attribute of the @Transactional annotation to optionally specify the identity of the TransactionManager to be used.

jakarta.transaction.Transactional vs org.springframework.transaction.annotation.Transactional

The jakarta.transaction.Transactional annotation is a standard annotation defined in the Jakarta Transaction API. The org.springframework.transaction.annotation.Transactional annotation is a Spring-specific annotation that provides additional features beyond the standard jakarta.transaction.Transactional annotation. The Spring-specific org.springframework.transaction.annotation.Transactional annotation is more commonly used in Spring applications.

Reference