A Comprehensive Overview of Spring Transaction Management
A transaction is a logical unit of work that consists of one or more operations, all of which must complete successfully, or none of them take effect.
In application development, a bussiness method often involves multiple atomic database operations. For example, the savePerson() method below includes two such operations. These operations are interdependent and must be executed as a single unit.
public void savePerson() {
personRepository.save(person);
personDetailRepository.save(personDetail);
}
Crucially, transaction support depends on the underlying database engine. For instance, MySQL's default innodb engine supports transactions. However, switching to the myisam engine would remove transaction support entirely.
The classic example is a bank transfer. Transferring 1000 units from account A to account B involves two key operations:
- Deduct 1000 from account A.
- Add 1000 to account B. A transaction ensures both operations succeed or fail together, preventing inconsistencies if an error occurs between them.
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false, timeout = -1)
public void transferFunds() {
// Credit the recipient
accountRepository.increaseBalance(1000, "recipient");
// Simulate a runtime exception (e.g., system failure)
// Without proper transaction management, the recipient would be credited but the sender not debited.
int result = 10 / 0;
// Debit the sender
accountRepository.decreaseBalance(1000, "sender");
}
}
Database transactions are defined by the ACID properties:
- Atomicity: All operations in a transaction complete successfully, or none are applied. The transaction is an indivisible unit.
- Consistency: A transaction transitions the database from one valid state to another, preserving all defined rules and constraints.
- Isolation: Concurrent transactions execute in isolation, preventing interference. Different isolation levels offer varying guarantees (e.g., Read Committed, Serializable).
- Durability: Once committed, the changes made by a transaction are permanent, surviving subsequent system failures.
Spring Framework Transaction Support
Reminder: Transaction capability is first determined by the database (e.g., MySQL with innodb). Using myisam fundamentally disables transactions.
How does MySQL ensure atomicity? It uses an undo log. All modifications within a transaction are first recorded in this log before being applied. If an error occurs, the undo log is used to revert changes. This log is persisted to disk before the actual data, ensuring recovery after a crash.
Transaction Management Approaches in Spring
Spring provides two primary ways to manage transactions.
1. Programmatic Transaction Management
This involves manually managing transactions using TransactionTemplate or PlatformTransactionManager. It's less common but useful for understanding Spring's transaction mechanics.
Example using TransactionTemplate:
@Service
public class ProgrammaticTxService {
@Autowired
private TransactionTemplate txTemplate;
public void executeWithTransaction() {
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
// Business logic here
} catch (Exception e) {
status.setRollbackOnly(); // Mark for rollback
}
}
});
}
}
Example using PlatformTransactionManager directly:
@Service
public class ProgrammaticTxService2 {
@Autowired
private PlatformTransactionManager txManager;
public void executeWithTransaction() {
TransactionStatus status = txManager.getTransaction(new DefaultTransactionDefinition());
try {
// Business logic here
txManager.commit(status);
} catch (Exception e) {
txManager.rollback(status);
}
}
}
2. Declarative Transaction Management
This is the recommended approach due to minimal code intrusion. It is implemented using AOP, most commonly via the @Transactional annotation.
Example:
@Transactional(propagation = Propagation.REQUIRED)
public void processOrder() {
// Business operations
inventoryService.updateStock();
paymentService.processPayment();
}
Core Spring Transaction Interfaces
Three key interfaces form the foundation of Spring's transaction abstraction:
PlatformTransactionManager: The core platform-specific transaction manager.TransactionDefinition: Defines transaction properties (isolation, propagation, timeout, etc.).TransactionStatus: Represents the current state of a transaction.
PlatformTransactionManager uses TransactionDefinition to manage transactions according to the specified properties, while TransactionStatus provides insight into the transaction's progress.
PlatformTransactionManager Interface
Spring delegates to platform-specific implementations of this interface (e.g., DataSourceTransactionManager for JDBC, JpaTransactionManager for JPA).
public interface PlatformTransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
TransactionDefinition Interface
This interface defines the configurable properties of a transaction.
public interface TransactionDefinition {
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
// ... other propagation constants
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
// ... other isolation constants
int TIMEOUT_DEFAULT = -1;
int getPropagationBehavior();
int getIsolationLevel();
int getTimeout();
boolean isReadOnly();
@Nullable
String getName();
}
TransactionStatus Interface
This interface provides methods to query and influence the transaction state.
public interface TransactionStatus extends SavepointManager {
boolean isNewTransaction();
boolean hasSavepoint();
void setRollbackOnly();
boolean isRollbackOnly();
boolean isCompleted();
}
Detailed Transaction Attributes
Transaction Propagation Behavior
Propagation defines how transactions relate when methods call eachother. Spring's Propagation enum encapsulates these behaviors.
Key propagation behaviors:
REQUIRED(Default): If a transaction exists, join it. Otherwise, create a new one. All methods usingREQUIREDwithin the same outer scope participate in the same transaction.REQUIRES_NEW: Always creates a new, independent transaction, suspending the current one if it exists. The inner transaction commits or rolls back independently of the outer one.NESTED: Executes within a nested transaction (a savepoint) if a current transaction exists. The nested transaction can roll back independently, but a rollback of the outer transaction forces a rollback of all nested ones. If no current transaction exists, it behaves likeREQUIRED. Supported by specific datasources like JDBC.MANDATORY: Must run within an existing transaction; throws an exception otherwise.SUPPORTS: Runs within an existing transaction if present; otherwise, executes non-transactionally.NOT_SUPPORTED: Always executes non-transactionally, suspending any existing transaction.NEVER: Must not run within a transaction; throws an exception if one exists.
Transaction Isolation Level
Isolation controls the visibility of changes made by concurrent transactions. Spring's Isolation enum provides standard levels.
DEFAULT: Uses the underlying database's default isolation level.READ_UNCOMMITTED: Allows reading uncommitted data (dirty reads). Lowest isolation, prone to anomalies.READ_COMMITTED: Prevents dirty reads; a transaction sees only committed data. Phantom reads and non-repeatable reads are still possible.REPEATABLE_READ: Ensures that rows read in a transaction cannot be changed by others, preventing dirty and non-repeatable reads. Phantom reads may still occur.SERIALIZABLE: Highest isolation. Transactions execute serially, preventing all anomalies (dirty, non-repeatable, phantom reads). Impacts performance.
MySQL Note: InnoDB's default REPEATABLE_READ level uses next-key locks to prevent phantom reads, offering stronger guarantees than the SQL standard for this level.
Transaction Timeout
Defines the maximum time (in seconds) a transaction can run before being automatically rolled back. The default -1 indicates no timeout.
Read-Only Hint
Marking a transaction as readOnly (default false) is a hint to the database and underlying frameworks for potential optimizations, as no data modification is expected.
@Transactional(readOnly = true)
public List<Order> findRecentOrders() { ... }
Why use a transaction for queries? Without @Transactional, each SQL statement in a method may run in its own auto-commit transaction. Wrapping multiple queries in a single read-only transaction ensures a consistent view of data for the duration of the method execution.
Transaction Rollback Rules
By default, a transaction rolls back only on runtime exceptions (RuntimeException and its subclasses) and Error. Checked exceptions (Exception) do not trigger a rollback.
You can customize this behavior:
@Transactional(rollbackFor = {BusinessException.class, IOException.class})
public void processData() throws BusinessException, IOException { ... }
Using the @Transactional Annotation
Scope
- Method (Recommended): Apply to public methods. Annotations on non-public methods are ignored by Spring's AOP proxy.
- Class: When applied at the class level, the annotation affects all public methods of that class.
- Interface (Not Recommended): Avoid using on interfaces as it may not behave consistently across different proxy mechanisms.
Key Configuration Parameters
| Parameter | Description | Default |
|---|---|---|
propagation |
Transaction propagation behavior. | Propagation.REQUIRED |
isolation |
Transaction isolation level. | Isolation.DEFAULT |
timeout |
Transaction timeout in seconds. | -1 (no timeout) |
readOnly |
Hint for a read-only transaction. | false |
rollbackFor / rollbackForClassName |
Exception type(s) that should trigger rollback. | RuntimeException, Error |
noRollbackFor / noRollbackForClassName |
Exception type(s) that should not trigger rollback. |
Implementation Mechanism
@Transactional is implemented using Spring AOP, which in turn relies on dynamic proxies.
- If the target object implements an interface, JDK dynamic proxies are used by default.
- If no interface is present, CGLIB is used to create the proxy.
When a @Transactional method is invoked, the call is intercepted. The TransactionInterceptor (via TransactionAspectSupport) handles the transaction lifecycle: starting a transaction before the method, committing on successful completion, or rolling back if an eligible exception is thrown.
Self-Invocation Pitfall
Calling a @Transactional method from within the same class (via a non-proxied method) bypasses the AOP proxy, causing the transactional behavior to be lost.
@Service
public class OrderService {
public void placeOrder() {
// This internal call bypasses the proxy -> @Transactional has no effect.
updateInventory();
}
@Transactional
public void updateInventory() {
// This transactional logic won't be executed within a transaction.
}
}
Solutions:
- Refactor to call the transactional method from another bean.
- Inject the proxy of the bean into itself (using
@Autowired). - Use AspectJ weaving (compile-time or load-time) instead of Spring AOP proxies.
Important Considerations
@Transactionalonly works on public methods.- Avoid self-invocation within the same class.
- Carefully configure
rollbackForfor custom checked exceptions that should cause rollback. - Understand the chosen propagation behavior to avoid unintended transaction boundaries.
- The
timeoutattribute may not be supported by all resource managers. - Declarative transaction management applies only to external method calls coming through the proxy.