SP
Transactions
Spring core v1.0.0
Spring Transaction Management
Overview
Spring’s declarative transaction management provides a consistent programming model across different transaction APIs (JDBC, JPA, JTA). This skill covers transaction propagation, isolation levels, rollback rules, and distributed transaction patterns.
Key Concepts
Transaction Properties (ACID)
| Property | Description |
|---|---|
| Atomicity | All or nothing - transaction completes fully or rolls back |
| Consistency | Database moves from valid state to valid state |
| Isolation | Concurrent transactions don’t interfere |
| Durability | Committed changes persist |
Isolation Levels
┌─────────────────────────────────────────────────────────────┐
│ Isolation Levels │
├─────────────────────────────────────────────────────────────┤
│ │
│ READ_UNCOMMITTED ← Dirty reads possible │
│ ↓ │
│ READ_COMMITTED ← Default for most DBs │
│ ↓ No dirty reads │
│ REPEATABLE_READ ← Same read returns same data │
│ ↓ │
│ SERIALIZABLE ← Full isolation (slowest) │
│ │
│ Phenomena: │
│ ┌─────────────────┬───────┬──────────┬───────────────┐ │
│ │ Level │Dirty │Non-repeat│Phantom │ │
│ │ │Read │Read │Read │ │
│ ├─────────────────┼───────┼──────────┼───────────────┤ │
│ │READ_UNCOMMITTED │ ✓ │ ✓ │ ✓ │ │
│ │READ_COMMITTED │ ✗ │ ✓ │ ✓ │ │
│ │REPEATABLE_READ │ ✗ │ ✗ │ ✓ │ │
│ │SERIALIZABLE │ ✗ │ ✗ │ ✗ │ │
│ └─────────────────┴───────┴──────────┴───────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Propagation Behaviors
| Propagation | Description |
|---|---|
| REQUIRED | Join existing or create new (default) |
| REQUIRES_NEW | Always create new, suspend existing |
| SUPPORTS | Join if exists, otherwise non-transactional |
| NOT_SUPPORTED | Suspend existing, run non-transactional |
| MANDATORY | Must have existing, throw if none |
| NEVER | Throw if transaction exists |
| NESTED | Nested transaction with savepoint |
Best Practices
1. Use Declarative Transactions
Prefer @Transactional over programmatic transaction management.
2. Keep Transactions Short
Long transactions hold locks and reduce concurrency.
3. Mark Read-Only Where Appropriate
@Transactional(readOnly = true) enables optimizations.
4. Handle Rollback Explicitly
Define which exceptions trigger rollback.
5. Be Careful with Self-Invocation
Calling a transactional method from within the same class bypasses the proxy.
Code Examples
Example 1: Basic Transaction Configuration
@Service
@RequiredArgsConstructor
@Transactional // Class-level default
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final InventoryService inventoryService;
private final EventPublisher eventPublisher;
// Uses class-level @Transactional (REQUIRED propagation)
public Order createOrder(CreateOrderCommand command) {
Order order = Order.create(command);
order = orderRepository.save(order);
// All these operations are in the same transaction
inventoryService.reserve(order.getItems());
paymentService.authorize(order.getPaymentDetails());
eventPublisher.publish(new OrderCreatedEvent(order));
return order;
}
// Override for read-only operations
@Transactional(readOnly = true)
public Order findById(OrderId orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
// Explicit rollback rules
@Transactional(
rollbackFor = BusinessException.class,
noRollbackFor = WarningException.class
)
public Order processOrder(OrderId orderId) {
Order order = findById(orderId);
try {
order.process();
return orderRepository.save(order);
} catch (InsufficientInventoryException e) {
// This will rollback (extends BusinessException)
throw e;
} catch (WarningException e) {
// This won't rollback
log.warn("Non-critical issue", e);
return order;
}
}
// Timeout for long operations
@Transactional(timeout = 30) // 30 seconds
public Order processLargeOrder(OrderId orderId) {
// Complex processing that might take time
return orderRepository.findById(orderId)
.map(this::processAllItems)
.orElseThrow();
}
}
Example 2: Propagation Behaviors
@Service
@RequiredArgsConstructor
@Slf4j
public class PropagationExamples {
private final OrderRepository orderRepository;
private final AuditService auditService;
private final NotificationService notificationService;
// REQUIRED (default) - joins existing or creates new
@Transactional(propagation = Propagation.REQUIRED)
public void requiredExample() {
// If called within existing transaction, joins it
// If no transaction exists, creates one
orderRepository.save(order);
}
// REQUIRES_NEW - always creates new transaction
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void requiresNewExample(AuditEntry entry) {
// Current transaction is suspended
// New transaction is created
// Useful for audit logs that must persist even if parent rolls back
auditService.save(entry);
}
// SUPPORTS - uses transaction if available
@Transactional(propagation = Propagation.SUPPORTS)
public Order getOrder(OrderId orderId) {
// Works with or without transaction
// Read-only operations often use this
return orderRepository.findById(orderId).orElseThrow();
}
// MANDATORY - must have existing transaction
@Transactional(propagation = Propagation.MANDATORY)
public void mandatoryExample() {
// Throws if no transaction exists
// Useful for methods that should never be called standalone
}
// NESTED - creates savepoint within existing transaction
@Transactional(propagation = Propagation.NESTED)
public void nestedExample() {
// Rolls back to savepoint on failure
// Parent transaction can choose to continue
// Note: Not all databases support savepoints
}
// Practical example: Order with independent audit logging
@Transactional
public Order createOrderWithAudit(CreateOrderCommand command) {
Order order = Order.create(command);
order = orderRepository.save(order);
try {
// Audit should persist even if notification fails
auditService.logOrderCreation(order); // REQUIRES_NEW
} catch (Exception e) {
log.error("Audit failed", e);
// Don't fail order creation for audit failure
}
try {
// Notification failure shouldn't rollback order
notificationService.notifyOrderCreated(order); // REQUIRES_NEW
} catch (Exception e) {
log.warn("Notification failed", e);
}
return order;
}
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOrderCreation(Order order) {
// Independent transaction - persists regardless of caller
auditRepository.save(new AuditEntry("ORDER_CREATED", order.getId()));
}
}
Example 3: Isolation Levels
@Service
@RequiredArgsConstructor
public class IsolationExamples {
// READ_COMMITTED - default, prevents dirty reads
@Transactional(isolation = Isolation.READ_COMMITTED)
public BigDecimal getAccountBalance(AccountId accountId) {
return accountRepository.findBalanceById(accountId);
}
// REPEATABLE_READ - prevents non-repeatable reads
@Transactional(isolation = Isolation.REPEATABLE_READ)
public TransferResult transfer(AccountId from, AccountId to, BigDecimal amount) {
// Reading same account twice will return same value
BigDecimal fromBalance = accountRepository.findBalanceById(from);
if (fromBalance.compareTo(amount) < 0) {
throw new InsufficientFundsException(from, amount);
}
// Even if another transaction modifies, we see consistent data
accountRepository.debit(from, amount);
accountRepository.credit(to, amount);
return TransferResult.success(from, to, amount);
}
// SERIALIZABLE - strongest isolation
@Transactional(isolation = Isolation.SERIALIZABLE)
public void criticalFinancialOperation() {
// Full isolation, no concurrent modifications
// Use sparingly - can cause lock contention
}
// Optimistic locking as alternative to pessimistic isolation
@Transactional
public Order updateOrderOptimistic(OrderId orderId, UpdateOrderCommand command) {
Order order = orderRepository.findById(orderId)
.orElseThrow();
// @Version field in entity handles optimistic locking
order.update(command);
try {
return orderRepository.save(order);
} catch (OptimisticLockingFailureException e) {
throw new ConcurrentModificationException(
"Order was modified by another transaction", e);
}
}
}
// Entity with optimistic locking
@Entity
public class Order {
@Id
private OrderId id;
@Version
private Long version; // Incremented on each update
// ...
}
Example 4: Programmatic Transactions
@Service
@RequiredArgsConstructor
public class ProgrammaticTransactionExamples {
private final TransactionTemplate transactionTemplate;
private final PlatformTransactionManager transactionManager;
private final OrderRepository orderRepository;
// TransactionTemplate for simple cases
public Order createOrderProgrammatic(CreateOrderCommand command) {
return transactionTemplate.execute(status -> {
try {
Order order = Order.create(command);
return orderRepository.save(order);
} catch (BusinessException e) {
status.setRollbackOnly();
throw e;
}
});
}
// Fine-grained control with TransactionManager
public void complexOperation() {
TransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def);
try {
// Multiple operations
step1();
step2();
step3();
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
// Mixing declarative and programmatic
@Transactional
public void mixedApproach() {
// Outer transaction from annotation
// Inner programmatic transaction
transactionTemplate.setPropagationBehavior(
TransactionDefinition.PROPAGATION_REQUIRES_NEW);
transactionTemplate.execute(status -> {
// Independent transaction
auditLog("operation started");
return null;
});
// Continue with outer transaction
performMainOperation();
}
// Read-only template
public Order findOrderReadOnly(OrderId orderId) {
TransactionTemplate readOnlyTemplate = new TransactionTemplate(transactionManager);
readOnlyTemplate.setReadOnly(true);
return readOnlyTemplate.execute(status ->
orderRepository.findById(orderId).orElseThrow()
);
}
}
Example 5: Transaction Event Listeners
@Service
@RequiredArgsConstructor
public class TransactionEventExamples {
private final ApplicationEventPublisher eventPublisher;
@Transactional
public Order createOrder(CreateOrderCommand command) {
Order order = Order.create(command);
order = orderRepository.save(order);
// Event published, but listeners may run at different phases
eventPublisher.publishEvent(new OrderCreatedEvent(order));
return order;
}
}
@Component
@Slf4j
public class OrderEventListener {
// Runs after transaction commits successfully
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
log.info("Order committed, sending notification");
// Safe to notify external systems
notificationService.sendOrderConfirmation(event.getOrder());
}
// Runs after transaction rolls back
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void onOrderRollback(OrderCreatedEvent event) {
log.warn("Order creation rolled back: {}", event.getOrder().getId());
// Cleanup any external state
}
// Runs before commit (can still cause rollback)
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void validateBeforeCommit(OrderCreatedEvent event) {
if (!validator.isValid(event.getOrder())) {
throw new ValidationException("Order invalid");
}
}
// Fallback if no transaction (useful for testing)
@TransactionalEventListener(fallbackExecution = true)
public void handleWithFallback(OrderCreatedEvent event) {
// Executes immediately if no transaction
}
}
// Custom transaction synchronization
@Component
public class TransactionCallback implements TransactionSynchronization {
@Override
public void beforeCommit(boolean readOnly) {
// Called before commit
}
@Override
public void afterCommit() {
// Called after successful commit
}
@Override
public void afterCompletion(int status) {
if (status == STATUS_COMMITTED) {
// Handle commit
} else if (status == STATUS_ROLLED_BACK) {
// Handle rollback
}
}
}
@Service
public class ServiceWithCallback {
@Transactional
public void operationWithCallback() {
// Register callback for current transaction
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
// Cleanup after commit
}
}
);
// Perform operations
}
}
Anti-Patterns
❌ Self-Invocation Bypasses Proxy
@Service
public class BrokenService {
public void outerMethod() {
// This calls the actual method, not the proxy!
innerMethod(); // No transaction!
}
@Transactional
public void innerMethod() {
// Expected transaction, but it won't work
}
}
// ✅ Fix with self-injection
@Service
public class FixedService {
@Lazy @Autowired
private FixedService self;
public void outerMethod() {
self.innerMethod(); // Calls through proxy
}
@Transactional
public void innerMethod() {
// Transaction works now
}
}
❌ Catching Exceptions That Should Rollback
// WRONG - transaction commits despite error
@Transactional
public void brokenRollback() {
try {
orderRepository.save(order);
paymentService.charge(order); // Throws exception
} catch (PaymentException e) {
log.error("Payment failed", e);
// Transaction still commits!
}
}
// ✅ CORRECT - rethrow or mark for rollback
@Transactional
public void correctRollback() {
try {
orderRepository.save(order);
paymentService.charge(order);
} catch (PaymentException e) {
throw e; // Rethrow to trigger rollback
}
}
Testing Strategies
@SpringBootTest
@Transactional // Rolls back after each test
class OrderServiceTest {
@Autowired
private OrderService orderService;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldCreateOrder() {
Order order = orderService.createOrder(command);
entityManager.flush();
entityManager.clear();
Order found = entityManager.find(Order.class, order.getId());
assertThat(found).isNotNull();
}
@Test
void shouldRollbackOnPaymentFailure() {
when(paymentService.charge(any())).thenThrow(new PaymentException());
assertThatThrownBy(() -> orderService.createOrder(command))
.isInstanceOf(PaymentException.class);
// Order should not exist
assertThat(orderRepository.count()).isZero();
}
}