Deep understanding of the @Transactional Annotation in Spring Framework
Introduction
The @Transactional
annotation in the Spring Framework is a powerful feature that simplifies transaction management in Java applications. Transactions are a critical aspect of enterprise applications, ensuring data integrity and consistency. The @Transactional
annotation provides declarative transaction management, which allows developers to manage transactions with minimal boilerplate code.
What is @Transactional?
The @Transactional
annotation indicates that the annotated method or class should be executed within a transactional context. This means that Spring will handle the opening, committing, and rolling back of transactions automatically, based on the configuration and execution outcome.
Purpose of @Transactional
The primary purpose of the @Transactional
annotation is to ensure data integrity and consistency by managing transactions declaratively. It helps to:
Maintain consistency across multiple operations.
Ensure that a series of operations either complete successfully or not at all.
Simplify the codebase by reducing the need for explicit transaction management code.
Benefits of Using @Transactional
Declarative Transaction Management: Simplifies the code by reducing boilerplate code for transaction management.
Automatic Rollback: Automatically rolls back transactions in case of runtime exceptions.
Consistency and Integrity: Ensures data consistency and integrity across multiple operations.
Customizable: Allows fine-grained control over transaction behavior using various attributes like isolation level, propagation behavior, timeout, etc.
Integration with Spring: Seamlessly integrates with other Spring components, enhancing the overall application structure and maintainability.
Best Environments for @Transactional
The @Transactional
annotation is best suited for environments where:
There are multiple operations that need to be executed as a single unit of work.
Data integrity and consistency are critical.
There is a need for a simplified and declarative approach to transaction management.
Enterprise applications with complex transaction requirements.
When Not to Use @Transactional
The @Transactional
annotation might not be suitable in environments where:
Operations do not require transactional consistency.
Performance is a critical concern, and the overhead of transaction management might be prohibitive.
There is a need for fine-grained manual control over transaction boundaries.
Simple Examples
- Simple Transactional Method
@Service
public class SimpleService {
@Autowired
private SimpleRepository repository;
@Transactional
public void performTransactionalOperation() {
// Operation 1
repository.save(new Entity());
// Operation 2
repository.update(new Entity());
}
}
- Transactional Read Operation
@Service
public class SimpleReadService {
@Autowired
private SimpleRepository repository;
@Transactional(readOnly = true)
public List<Entity> fetchEntities() {
return repository.findAll();
}
}
Complex Examples
- Nested Transactions
@Service
public class NestedTransactionService {
@Autowired
private SimpleRepository repository;
@Autowired
private AnotherService anotherService;
@Transactional
public void performNestedTransaction() {
// Operation 1
repository.save(new Entity());
// Nested Transaction
anotherService.performAnotherOperation();
// Operation 2
repository.update(new Entity());
}
}
@Service
public class AnotherService {
@Autowired
private AnotherRepository repository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void performAnotherOperation() {
// Independent Operation
repository.save(new AnotherEntity());
}
}
- Transaction with Rollback for Specific Exception
@Service
public class RollbackService {
@Autowired
private SimpleRepository repository;
@Transactional(rollbackFor = CustomException.class)
public void performOperationWithRollback() throws CustomException {
// Operation 1
repository.save(new Entity());
// Simulate exception
if (someCondition) {
throw new CustomException("Rollback this transaction");
}
// Operation 2
repository.update(new Entity());
}
}
How Spring Handles Rollback and Processes Behind the Scenes
When a method annotated with @Transactional
is called, Spring manages the transaction lifecycle as follows:
Transaction Start: Spring checks the transactional context and starts a new transaction if none exists, based on the propagation setting.
Transaction Management: The transactional context is propagated to all methods and operations within the transactional scope.
Commit or Rollback:
Commit: If the method completes successfully without exceptions, Spring commits the transaction, making all changes permanent.
Rollback: If a runtime exception occurs (or a specified exception type using
rollbackFor
), Spring rolls back the transaction, undoing all changes made during the transaction.
Behind the Scenes: Rollback Process
Exception Detection: Spring detects an exception during the execution of the transactional method.
Transaction Rollback:
Spring intercepts the exception and marks the transaction for rollback.
It calls the
rollback()
method on the transaction manager, which undoes all changes made during the transaction.
Resource Cleanup: Spring ensures that all resources (e.g., database connections) are properly cleaned up, regardless of whether the transaction is committed or rolled back.
Spring's transaction management relies on a PlatformTransactionManager
implementation, typically DataSourceTransactionManager
for JDBC or JpaTransactionManager
for JPA. This manager coordinates the transaction lifecycle, ensuring that transactions are correctly handled across different transactional resources.
Parameters of the @Transactional Annotation
The @Transactional
annotation in the Spring Framework offers various parameters that allow customization of transaction behavior. Below, we list and explain each of these parameters:
1. Propagation
Defines how the current transaction should behave in relation to existing transactions. The possible values are:
REQUIRED (default): If a transaction already exists, it will be used; otherwise, a new transaction will be started. This is the most common propagation setting. It ensures that all operations within the method are part of a single transaction.
Example:
@Transactional(propagation = Propagation.REQUIRED) public void requiredOperation() { // Operations here will use the existing transaction or start a new one }
SUPPORTS: If a transaction already exists, it will be used; otherwise, the method will be executed without a transaction. This is useful for methods that can operate with or without a transaction.
Example:
@Transactional(propagation = Propagation.SUPPORTS) public void supportsOperation() { // Operations here will use the existing transaction if there is one, // otherwise, they will execute non-transactionally }
MANDATORY: Requires an existing transaction; throws an exception if there is no transaction. This setting ensures that the method must be called within an existing transaction context.
Example:
@Transactional(propagation = Propagation.MANDATORY) public void mandatoryOperation() { // Operations here will throw an exception if there is no existing transaction }
REQUIRES_NEW: Always starts a new transaction, suspending the current transaction if one exists. This setting is used when you want to ensure that the method runs in its own transaction, independent of the caller’s transaction.
Example:
@Transactional(propagation = Propagation.REQUIRES_NEW) public void requiresNewOperation() { // Operations here will run in a new transaction, suspending any existing one }
NOT_SUPPORTED: The method is executed outside of any transaction context, suspending the current transaction if one exists. This is useful for non-transactional operations that might be called within a transactional context.
Example:
@Transactional(propagation = Propagation.NOT_SUPPORTED) public void notSupportedOperation() { // Operations here will run outside of any transaction context }
NEVER: Throws an exception if a transaction exists; otherwise, executes the method outside of a transaction. This ensures that the method never runs within a transactional context.
Example:
@Transactional(propagation = Propagation.NEVER) public void neverOperation() { // Operations here will throw an exception if there is an existing transaction }
NESTED: If a transaction already exists, creates a nested transaction; otherwise, behaves like
REQUIRED
. This is used for creating savepoints within a transaction, allowing partial rollbacks.Example:
@Transactional(propagation = Propagation.NESTED) public void nestedOperation() { // Operations here will run in a nested transaction if there is an existing one }
2. Isolation
Defines the isolation level of the transaction, controlling the visibility of changes made in concurrent transactions. The possible values are:
DEFAULT (default): Uses the default isolation level of the database. This level varies between different database systems but typically balances consistency and performance.
Example:
@Transactional(isolation = Isolation.DEFAULT) public void defaultIsolationOperation() { // Operations here will use the default isolation level of the database }
READ_UNCOMMITTED: Allows dirty reads, meaning one transaction can see changes made by other uncommitted transactions. This level has the highest risk of inconsistencies but offers the best performance.
Example:
@Transactional(isolation = Isolation.READ_UNCOMMITTED) public void readUncommittedOperation() { // Operations here can read uncommitted changes from other transactions }
READ_COMMITTED: Prevents dirty reads; a transaction can only see committed changes made by other transactions. This is the most common isolation level, providing a balance between consistency and performance.
Example:
@Transactional(isolation = Isolation.READ_COMMITTED) public void readCommittedOperation() { // Operations here can only read committed changes from other transactions }
REPEATABLE_READ: Prevents dirty and non-repeatable reads; once a transaction reads data, it will see the same data throughout its duration. This level ensures consistency for repeated reads but can lead to phantom reads.
Example:
o@Transactional(isolation = Isolation.REPEATABLE_READ) public void repeatableReadOperation() { // Operations here will see the same data for repeated reads within the transaction }
SERIALIZABLE: The highest isolation level, ensuring full consistency by serializing transactions. This level prevents dirty, non-repeatable, and phantom reads but can significantly impact performance due to high locking and blocking.
Example:
@Transactional(isolation = Isolation.SERIALIZABLE) public void serializableOperation() { // Operations here will be fully serialized, ensuring the highest consistency }
timeout
Defines the maximum time, in seconds, that the transaction can last before being automatically rolled back. The default value is -1, indicating no limit.
@Transactional(timeout = 5)
public void performOperation() {
// Operation that must be completed within 5 seconds
}
readOnly
Indicates whether the transaction is read-only, optimizing performance for operations that do not modify data.
@Transactional(readOnly = true)
public List<Entity> fetchEntities() {
return repository.findAll();
}
rollbackFor
Specifies the exceptions that should cause the transaction to roll back. Can be a single exception or an array of exceptions.
@Transactional(rollbackFor = CustomException.class)
public void performOperation() throws CustomException {
// Operation that will be rolled back if CustomException is thrown
}
noRollbackFor
Specifies the exceptions that should not cause the transaction to roll back, even if they are thrown.
@Transactional(noRollbackFor = CustomException.class)
public void performOperation() throws CustomException {
// Operation that will not be rolled back if CustomException is thrown
}
transactionManager
Specifies the name of the PlatformTransactionManager
to be used for the transaction.
@Transactional(transactionManager = "customTransactionManager")
public void performOperation() {
// Operation that will use a custom transaction manager
}
label
Adds a label to the transaction for monitoring and debugging purposes (introduced in Spring 5.3).
@Transactional(label = "transactionLabel")
public void performOperation() {
// Operation with a custom label for monitoring
}
value
Alias for the transactionManager
parameter, allowing you to specify the transaction manager.
@Transactional("customTransactionManager")
public void performOperation() {
// Operation that will use a custom transaction manager
}
Conclusion
The @Transactional
annotation in Spring Framework provides a robust and declarative approach to managing transactions in Java applications. By leveraging @Transactional
, developers can ensure data consistency and integrity while simplifying the codebase. Understanding the benefits, use cases, and the underlying mechanisms of @Transactional
can significantly enhance the effectiveness of transaction management in Spring-based applications.
Subscribe to my newsletter
Read articles from André Felipe Costa Bento directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
André Felipe Costa Bento
André Felipe Costa Bento
Fullstack Software Engineer.