Spring Transaction Propagation Guide
Spring Transaction Propagation: Complete Guide
Transaction Propagation in Spring
When service methods call other service methods, Spring manages how transactions “flow” between them. This is controlled by the propagation
attribute in the @Transactional
annotation.
Basic Scenarios
Default Behavior (REQUIRED)
- When
ServiceA.methodA()
callsServiceB.methodB()
, both execute in the same transaction - If any method throws an exception, everything rolls back
- This is what you’re using in your code, which is appropriate for most cases
Independent Transactions (REQUIRES_NEW)
- Creates a completely new transaction, suspending the current one
- If the inner method succeeds, its changes are committed even if the outer method fails
- Perfect for operations like logging or auditing that should persist regardless of the main transaction
Nested Transactions (NESTED)
- Creates a “savepoint” within the current transaction
- If the inner method fails, only its changes roll back
- If the outer method fails, everything rolls back
- Useful for batch processing where you want to process as many items as possible
Core Concepts
Transactions in Spring manage a sequence of database operations to be treated as a single unit. When service methods call other service methods, Spring needs to know how to handle the transaction behavior.
Key Transaction Attributes
- Propagation: Defines how transactions relate to each other when methods are nested
- Isolation: Defines the data visibility between concurrent transactions
- Timeout: Maximum time a transaction may take
- Read-Only: Hint to the database for optimization
- Rollback Rules: Define which exceptions cause rollback
Propagation Options
REQUIRED (Default)
- Behavior: Uses existing transaction if available; creates a new one if none exists
- Use Case: Most common scenario for business logic
- Example:
@Transactional(propagation = Propagation.REQUIRED)public void methodA() { // Uses its own transaction if called directly // OR uses caller's transaction if called from a transactional method serviceB.methodB(); // Joins methodA's transaction}
REQUIRES_NEW
- Behavior: Always creates a new transaction, suspending the current one if it exists
- Use Case: Operations that should commit/rollback independently
- Example:
@Transactional(propagation = Propagation.REQUIRES_NEW)public void methodB() { // Always runs in a new transaction // If methodA fails, methodB can still commit if already executed}
NESTED
- Behavior: Creates a savepoint within the current transaction
- Use Case: When you want the option to rollback part of a transaction
- Example:
@Transactional(propagation = Propagation.NESTED)public void methodC() { // Creates a savepoint in the existing transaction // If methodC fails, only its changes rollback // If outer transaction fails, everything rolls back}
SUPPORTS
- Behavior: Uses existing transaction if available, otherwise non-transactional
- Use Case: Methods that can work with or without a transaction
- Example:
@Transactional(propagation = Propagation.SUPPORTS)public void methodD() { // Runs in a transaction only if caller has one}
NOT_SUPPORTED
- Behavior: Executes non-transactionally, suspending current transaction if any
- Use Case: Operations that should not be part of a transaction (e.g., read-only operations)
- Example:
@Transactional(propagation = Propagation.NOT_SUPPORTED)public void methodE() { // Always runs non-transactionally // Existing transactions are suspended and resumed after}
NEVER
- Behavior: Executes non-transactionally, throws exception if transaction exists
- Use Case: Enforcing that a method must not be called within a transaction
- Example:
@Transactional(propagation = Propagation.NEVER)public void methodF() { // Throws exception if called from a transactional context}
MANDATORY
- Behavior: Must run within existing transaction, throws exception if none exists
- Use Case: Ensuring a method is only called within a transaction context
- Example:
@Transactional(propagation = Propagation.MANDATORY)public void methodG() { // Throws exception if no active transaction}
Common Use Cases
Case 1: All-or-Nothing Process
Use REQUIRED
for all methods to ensure all operations commit or roll back together.
Case 2: Independent Logging or Auditing
Use REQUIRES_NEW
for logging/auditing operations that should succeed even if the main transaction fails.
@Service
public class OrderService {
@Autowired private AuditService auditService;
@Transactional
public void processOrder(Order order) {
// Process order (may fail)
// Audit should succeed even if order processing fails
auditService.logOrderAttempt(order); // Uses REQUIRES_NEW
}
}
Case 3: Partial Rollback
Use NESTED
when you want the option to roll back part of a transaction without affecting the rest.
@Service
public class BatchProcessingService {
@Transactional
public void processBatch(List<Item> items) {
for (Item item : items) {
try {
// If one item fails, only its changes roll back
itemProcessor.process(item); // Uses NESTED
} catch (Exception e) {
// Handle error, but continue with other items
}
}
// Batch metadata still saved even if some items failed
}
}
Transaction Isolation Levels
Complement propagation with appropriate isolation levels:
- DEFAULT: Database default (usually READ_COMMITTED)
- READ_UNCOMMITTED: Lowest isolation, allows dirty reads (seeing uncommitted changes)
- READ_COMMITTED: Prevents dirty reads, but allows non-repeatable reads
- REPEATABLE_READ: Prevents non-repeatable reads, but allows phantom reads
- SERIALIZABLE: Highest isolation, prevents all concurrency anomalies
Example:
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED
)
public void methodH() {
// Uses READ_COMMITTED isolation level
}
Best Practices
- Keep transactions short: Long-running transactions lead to database contention
- Place @Transactional on service methods: Not on controllers or repositories
- Be aware of self-invocation: Calling a @Transactional method from within the same class bypasses the proxy
- Consider read-only for queries: Use
@Transactional(readOnly = true)
for optimization - Understand exception handling: Only unchecked exceptions trigger rollback by default
- Use specific rollback rules:
@Transactional(rollbackFor = {Exception.class})
to roll back on checked exceptions
Debugging Transaction Issues
Enable transaction logging:
logging.level.org.springframework.transaction=TRACE
logging.level.org.springframework.orm.jpa=DEBUG
Common Pitfalls to Avoid
- Self-invocation: Calling a
@Transactional
method from within the same class bypasses the proxy, so the transaction won't work as expected - Checked Exceptions: By default, Spring only rolls back on unchecked exceptions. Use
rollbackFor = Exception.class
to include checked exceptions (which you're already doing) - Transaction Visibility: Changes made within a transaction aren’t visible to other transactions until committed
The artifacts I’ve created provide a visual representation of how transactions flow in different scenarios, along with a comprehensive guide to Spring’s transaction management options. The diagram shows the three main propagation types, while the guide covers all options with code examples and use cases.