Spring Transaction Management
Master Spring Transaction Management with ACID properties, propagation behaviors, isolation levels, real-world examples, and best practices.
Spring Transaction Management
Table of Contents
- What is a Transaction?
- Transaction Management in Spring
- The @Transactional Annotation
- Transaction Propagation
- Transaction Isolation Levels
- Transaction Rollback
- Real-World Examples
- Best Practices
- Common Pitfalls
What is a Transaction?
A transaction is a sequence of operations performed as a single logical unit of work. All operations must succeed together, or all must fail together.
ACID Properties
Every transaction must guarantee four properties:
1. Atomicity - All or Nothing
Transfer $100 from Account A to Account B:
1. Deduct $100 from A
2. Add $100 to B
✅ Both operations succeed → Transaction commits
❌ Any operation fails → Both operations rollback
Example:
// Without atomicity
deductFromAccount(A, 100); // ✓ Success
// System crashes here!
addToAccount(B, 100); // ✗ Never executed
// Result: $100 disappeared!
// With atomicity
@Transactional
public void transfer() {
deductFromAccount(A, 100);
addToAccount(B, 100);
// Both succeed or both fail
}
2. Consistency - Valid State to Valid State
Before: A=$500, B=$300, Total=$800
After: A=$400, B=$400, Total=$800
✅ Total remains consistent
❌ Invalid states are prevented
3. Isolation - Concurrent Transactions Don't Interfere
Transaction 1: Transfer $100 from A to B
Transaction 2: Transfer $50 from A to C
Both run concurrently without seeing each other's intermediate states.
4. Durability - Committed Changes Persist
After commit, changes survive:
✅ System crashes
✅ Power failures
✅ Database restarts
Transaction Management in Spring
Without Transaction Management
@Service
public class BankService {
@Autowired
private AccountRepository accountRepository;
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// Step 1: Deduct from source
Account fromAccount = accountRepository.findById(fromId);
fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
accountRepository.save(fromAccount);
// ⚠️ If exception occurs here, money is lost!
// Source debited but destination not credited
// Step 2: Add to destination
Account toAccount = accountRepository.findById(toId);
toAccount.setBalance(toAccount.getBalance().add(amount));
accountRepository.save(toAccount);
}
}
Problem Flow:
1. Deduct $100 from Account A ✓
2. Exception occurs! ✗
3. Account B never credited ✗
Result: $100 disappeared from the system!
With Transaction Management
@Service
public class BankService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// Step 1: Deduct from source
Account fromAccount = accountRepository.findById(fromId);
fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
accountRepository.save(fromAccount);
// If exception occurs, entire transaction rolls back
// Step 2: Add to destination
Account toAccount = accountRepository.findById(toId);
toAccount.setBalance(toAccount.getBalance().add(amount));
accountRepository.save(toAccount);
// Both operations succeed or both fail
}
}
Transaction Flow:
BEGIN TRANSACTION
↓
1. Deduct $100 from Account A
↓
2. Exception occurs?
├─ Yes → ROLLBACK (restore Account A)
└─ No → Continue
↓
3. Add $100 to Account B
↓
4. All operations successful?
├─ Yes → COMMIT (make changes permanent)
└─ No → ROLLBACK (undo all changes)
Transaction Management Types
1. Programmatic Transaction Management
Manual control over transactions - You explicitly manage transaction boundaries.
@Service
public class BankService {
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private AccountRepository accountRepository;
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// Define transaction
TransactionDefinition def = new DefaultTransactionDefinition();
// Start transaction
TransactionStatus status = transactionManager.getTransaction(def);
try {
// Business logic
Account fromAccount = accountRepository.findById(fromId);
fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
accountRepository.save(fromAccount);
Account toAccount = accountRepository.findById(toId);
toAccount.setBalance(toAccount.getBalance().add(amount));
accountRepository.save(toAccount);
// Commit transaction
transactionManager.commit(status);
} catch (Exception e) {
// Rollback transaction
transactionManager.rollback(status);
throw e;
}
}
}
Pros:
- ✅ Fine-grained control
- ✅ Flexible for complex scenarios
Cons:
- ❌ Verbose and boilerplate code
- ❌ Error-prone
- ❌ Mixes business logic with transaction code
2. Declarative Transaction Management (Recommended)
Using @Transactional annotation - Spring manages transactions automatically.
@Service
public class BankService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// Clean business logic only
Account fromAccount = accountRepository.findById(fromId);
fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
accountRepository.save(fromAccount);
Account toAccount = accountRepository.findById(toId);
toAccount.setBalance(toAccount.getBalance().add(amount));
accountRepository.save(toAccount);
}
}
How It Works (Spring AOP):
Client calls transfer()
↓
AOP Proxy intercepts
↓
Begin Transaction
↓
Execute transfer() method
↓
Exception thrown?
├─ Yes → Rollback → Throw exception
└─ No → Commit → Return result
Pros:
- ✅ Clean, readable code
- ✅ Less boilerplate
- ✅ Easy to maintain
- ✅ Recommended approach
Cons:
- ❌ Less fine-grained control
- ❌ Proxy limitations (self-invocation issues)
The @Transactional Annotation
Basic Usage
// Class level - applies to all public methods
@Service
@Transactional
public class UserService {
public void createUser(User user) { }
public void updateUser(User user) { }
public void deleteUser(Long id) { }
}
// Method level - applies to specific method
@Service
public class UserService {
@Transactional
public void createUser(User user) { }
// No transaction
public User getUser(Long id) { }
}
Configuration Attributes
@Transactional(
propagation = Propagation.REQUIRED, // How transactions relate
isolation = Isolation.DEFAULT, // Isolation level
timeout = 30, // Timeout in seconds
readOnly = false, // Read-only optimization
rollbackFor = Exception.class, // Rollback on these exceptions
noRollbackFor = BusinessException.class // Don't rollback on these
)
public void complexOperation() {
// Transaction configuration applied
}
Transaction Propagation
Definition: How transactions relate to each other when one transactional method calls another.
1. REQUIRED (Default)
Behavior: Use existing transaction or create new one
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
// Transaction T1 starts
methodB(); // Uses same transaction T1
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
// Uses existing transaction T1
}
Flow:
methodA() called
↓
Transaction exists?
├─ Yes → Use existing transaction
└─ No → Create new transaction
↓
Execute methodA()
↓
Call methodB()
↓
Use same transaction
↓
Both methods in same transaction
↓
Commit or Rollback together
Use Case: Default behavior, most common scenario
2. REQUIRES_NEW
Behavior: Always create new transaction, suspend existing
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
// Transaction T1
methodB(); // Creates new transaction T2, suspends T1
// T1 resumes after T2 completes
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// New transaction T2
// Independent of T1
}
Flow:
methodA() - Transaction T1
↓
Call methodB()
↓
Suspend T1
↓
Create new Transaction T2
↓
Execute methodB()
↓
T2 commits/rollbacks independently
↓
Resume T1
↓
T1 continues
↓
T1 commits/rollbacks independently
Use Case: Logging, audit trails (must persist even if main transaction fails)
Example:
@Service
public class OrderService {
@Autowired
private AuditService auditService;
@Transactional
public void processOrder(Order order) {
// Transaction T1
orderRepository.save(order);
// Log audit - must persist even if order fails
auditService.logOrderAttempt(order); // Transaction T2
// If this fails, T1 rolls back but T2 already committed
paymentService.processPayment(order);
}
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOrderAttempt(Order order) {
// Transaction T2 - independent
auditRepository.save(new AuditLog(order));
// T2 commits immediately
}
}
3. NESTED
Behavior: Execute within nested transaction if exists, otherwise like REQUIRED
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
// Transaction T1
methodB(); // Nested transaction (savepoint)
}
@Transactional(propagation = Propagation.NESTED)
public void methodB() {
// Nested transaction with savepoint
// Can rollback to savepoint without affecting T1
}
Flow:
methodA() - Transaction T1
↓
Call methodB()
↓
Create Savepoint S1
↓
Execute methodB()
↓
Exception in methodB()?
├─ Yes → Rollback to S1 (T1 continues)
└─ No → Release S1
↓
Continue T1
↓
Commit T1
Use Case: Partial rollback scenarios
4. MANDATORY
Behavior: Must have existing transaction, throw exception if none
@Transactional(propagation = Propagation.MANDATORY)
public void methodB() {
// Must be called within existing transaction
// Throws exception if no transaction exists
}
Use Case: Enforce transaction requirement for critical operations
5. SUPPORTS
Behavior: Use transaction if exists, execute non-transactionally if none
@Transactional(propagation = Propagation.SUPPORTS)
public void methodB() {
// Uses transaction if exists
// Runs without transaction if none exists
}
Use Case: Read operations that can work with or without transactions
6. NOT_SUPPORTED
Behavior: Execute non-transactionally, suspend existing transaction
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void methodB() {
// Always executes without transaction
// Suspends existing transaction if any
}
Use Case: Operations that shouldn't be transactional (e.g., sending emails)
7. NEVER
Behavior: Execute non-transactionally, throw exception if transaction exists
@Transactional(propagation = Propagation.NEVER)
public void methodB() {
// Must NOT be called within transaction
// Throws exception if transaction exists
}
Use Case: Enforce non-transactional execution
Propagation Comparison Table
┌─────────────────┬──────────────┬─────────────┬──────────────┐
│ Propagation │ Has Trans? │ Action │ New Trans? │
├─────────────────┼──────────────┼─────────────┼──────────────┤
│ REQUIRED │ Yes │ Use it │ No │
│ (Default) │ No │ Create new │ Yes │
├─────────────────┼──────────────┼─────────────┼──────────────┤
│ REQUIRES_NEW │ Yes │ Suspend │ Yes │
│ │ No │ Create new │ Yes │
├─────────────────┼──────────────┼─────────────┼──────────────┤
│ NESTED │ Yes │ Savepoint │ No │
│ │ No │ Create new │ Yes │
├─────────────────┼──────────────┼─────────────┼──────────────┤
│ MANDATORY │ Yes │ Use it │ No │
│ │ No │ Exception │ N/A │
├─────────────────┼──────────────┼─────────────┼──────────────┤
│ SUPPORTS │ Yes │ Use it │ No │
│ │ No │ No trans │ No │
├─────────────────┼──────────────┼─────────────┼──────────────┤
│ NOT_SUPPORTED │ Yes │ Suspend │ No │
│ │ No │ No trans │ No │
├─────────────────┼──────────────┼─────────────┼──────────────┤
│ NEVER │ Yes │ Exception │ N/A │
│ │ No │ No trans │ No │
└─────────────────┴──────────────┴─────────────┴──────────────┘
Transaction Isolation Levels
Definition: Degree to which one transaction is isolated from others
Isolation Problems
1. Dirty Read
Reading uncommitted data from another transaction
Transaction 1: UPDATE account SET balance = 500 WHERE id = 1
Transaction 2: SELECT balance FROM account WHERE id = 1 → Reads 500
Transaction 1: ROLLBACK → Balance back to original
Problem: Transaction 2 read uncommitted data (dirty read)
2. Non-Repeatable Read
Same query returns different results within same transaction
Transaction 1: SELECT balance FROM account WHERE id = 1 → Reads 1000
Transaction 2: UPDATE account SET balance = 500 WHERE id = 1
Transaction 2: COMMIT
Transaction 1: SELECT balance FROM account WHERE id = 1 → Reads 500
Problem: Same query, different results (non-repeatable read)
3. Phantom Read
New rows appear in subsequent queries
Transaction 1: SELECT COUNT(*) FROM account WHERE balance > 1000 → Returns 5
Transaction 2: INSERT INTO account VALUES (6, 1500)
Transaction 2: COMMIT
Transaction 1: SELECT COUNT(*) FROM account WHERE balance > 1000 → Returns 6
Problem: New rows appeared (phantom read)
Isolation Levels
1. READ_UNCOMMITTED (Lowest Isolation)
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void method() {
// Can read uncommitted changes from other transactions
}
- Problems: ❌ Dirty read, ❌ Non-repeatable read, ❌ Phantom read
- Performance: ⚡ Fastest
- Use Case: Rarely used, only for non-critical data
2. READ_COMMITTED (Default in Most Databases)
@Transactional(isolation = Isolation.READ_COMMITTED)
public void method() {
// Can only read committed changes
}
- Problems: ✅ Prevents dirty read, ❌ Non-repeatable read, ❌ Phantom read
- Performance: ⚡ Good
- Use Case: Most common, good balance
3. REPEATABLE_READ
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void method() {
// Same query returns same results
}
- Problems: ✅ Prevents dirty read, ✅ Prevents non-repeatable read, ❌ Phantom read
- Performance: 🐌 Slower
- Use Case: When consistency within transaction is critical
4. SERIALIZABLE (Highest Isolation)
@Transactional(isolation = Isolation.SERIALIZABLE)
public void method() {
// Complete isolation, transactions execute serially
}
- Problems: ✅ Prevents all problems
- Performance: 🐌 Slowest
- Use Case: Critical financial transactions
Isolation Level Comparison
┌──────────────────┬─────────────┬──────────────────┬──────────────┬─────────────┐
│ Isolation Level │ Dirty Read │ Non-Repeatable │ Phantom Read │ Performance │
├──────────────────┼─────────────┼──────────────────┼──────────────┼─────────────┤
│ READ_UNCOMMITTED │ Possible │ Possible │ Possible │ Fastest │
├──────────────────┼─────────────┼──────────────────┼──────────────┼─────────────┤
│ READ_COMMITTED │ Prevented │ Possible │ Possible │ Fast │
├──────────────────┼─────────────┼──────────────────┼──────────────┼─────────────┤
│ REPEATABLE_READ │ Prevented │ Prevented │ Possible │ Slow │
├──────────────────┼─────────────┼──────────────────┼──────────────┼─────────────┤
│ SERIALIZABLE │ Prevented │ Prevented │ Prevented │ Slowest │
└──────────────────┴─────────────┴──────────────────┴──────────────┴─────────────┘
Transaction Rollback
Default Rollback Behavior
@Transactional
public void method() {
// Rolls back on RuntimeException and Error
// Does NOT rollback on checked exceptions
}
Rollback Decision Flow:
Method execution
↓
Exception thrown?
├─ RuntimeException → Rollback
├─ Error → Rollback
├─ Checked Exception → Commit (!)
└─ No exception → Commit
Custom Rollback Rules
// Rollback on all exceptions
@Transactional(rollbackFor = Exception.class)
public void method() {
// Rolls back on any exception
}
// Don't rollback on specific exceptions
@Transactional(noRollbackFor = BusinessException.class)
public void method() {
// Commits even if BusinessException thrown
}
// Combine rules
@Transactional(
rollbackFor = {SQLException.class, IOException.class},
noRollbackFor = {BusinessException.class}
)
public void method() {
// Custom rollback behavior
}
Real-World Examples
Example 1: E-commerce Order Processing
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private InventoryService inventoryService;
@Autowired
private PaymentService paymentService;
@Autowired
private NotificationService notificationService;
@Transactional(
isolation = Isolation.READ_COMMITTED,
timeout = 30,
rollbackFor = Exception.class
)
public Order processOrder(OrderRequest request) {
// 1. Create order
Order order = new Order(request);
order.setStatus(OrderStatus.PENDING);
orderRepository.save(order);
// 2. Reserve inventory
inventoryService.reserveItems(order.getItems());
// 3. Process payment
Payment payment = paymentService.processPayment(order);
order.setPayment(payment);
// 4. Update order status
order.setStatus(OrderStatus.CONFIRMED);
orderRepository.save(order);
// 5. Send notification (separate transaction)
notificationService.sendOrderConfirmation(order);
return order;
}
}
@Service
public class NotificationService {
// Separate transaction - must succeed even if order fails later
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendOrderConfirmation(Order order) {
emailService.send(
order.getCustomer().getEmail(),
"Order Confirmation",
"Your order " + order.getId() + " is confirmed"
);
}
}
Transaction Flow:
processOrder() called
↓
Transaction T1 begins
↓
1. Save order (PENDING)
↓
2. Reserve inventory
↓
3. Process payment
↓
4. Update order (CONFIRMED)
↓
5. Send notification
├─ Suspend T1
├─ Transaction T2 begins
├─ Send email
├─ Commit T2
└─ Resume T1
↓
All successful?
├─ Yes → Commit T1 → Order confirmed
└─ No → Rollback T1 → Order cancelled
→ But email already sent (T2 committed)
Example 2: Banking Transfer with Audit
@Service
public class BankingService {
@Autowired
private AccountRepository accountRepository;
@Autowired
private AuditService auditService;
@Transactional(
isolation = Isolation.SERIALIZABLE, // Highest isolation for money
timeout = 10,
rollbackFor = Exception.class
)
public TransferResult transfer(Long fromId, Long toId, BigDecimal amount) {
// Log attempt (must persist even if transfer fails)
auditService.logTransferAttempt(fromId, toId, amount);
// 1. Validate accounts
Account fromAccount = accountRepository.findById(fromId)
.orElseThrow(() -> new AccountNotFoundException(fromId));
Account toAccount = accountRepository.findById(toId)
.orElseThrow(() -> new AccountNotFoundException(toId));
// 2. Validate balance
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException();
}
// 3. Perform transfer
fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
toAccount.setBalance(toAccount.getBalance().add(amount));
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
// 4. Log success
auditService.logTransferSuccess(fromId, toId, amount);
return new TransferResult(true, "Transfer successful");
}
}
@Service
public class AuditService {
@Autowired
private AuditRepository auditRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logTransferAttempt(Long fromId, Long toId, BigDecimal amount) {
AuditLog log = new AuditLog();
log.setType("TRANSFER_ATTEMPT");
log.setFromAccount(fromId);
log.setToAccount(toId);
log.setAmount(amount);
log.setTimestamp(LocalDateTime.now());
auditRepository.save(log);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logTransferSuccess(Long fromId, Long toId, BigDecimal amount) {
AuditLog log = new AuditLog();
log.setType("TRANSFER_SUCCESS");
log.setFromAccount(fromId);
log.setToAccount(toId);
log.setAmount(amount);
log.setTimestamp(LocalDateTime.now());
auditRepository.save(log);
}
}
Complete Flow:
transfer() called
↓
Log attempt (T2 - REQUIRES_NEW)
├─ Suspend T1
├─ Begin T2
├─ Save audit log
├─ Commit T2 (persisted)
└─ Resume T1
↓
Transaction T1 continues
↓
Validate accounts
↓
Check balance
↓
Insufficient funds?
├─ Yes → Throw exception
│ ↓
│ Rollback T1
│ ↓
│ Transfer failed
│ ↓
│ But audit log persisted (T2)
│
└─ No → Continue
↓
Deduct from source
↓
Add to destination
↓
Save both accounts
↓
Log success (T3 - REQUIRES_NEW)
├─ Suspend T1
├─ Begin T3
├─ Save success log
├─ Commit T3
└─ Resume T1
↓
Commit T1
↓
Transfer successful
Best Practices
1. Keep Transactions Short
// ❌ Bad: Long transaction
@Transactional
public void processOrder(Order order) {
orderRepository.save(order);
// Long-running operation
emailService.sendConfirmation(order); // 5 seconds
// External API call
shippingService.scheduleDelivery(order); // 10 seconds
// Transaction held for 15+ seconds!
}
// ✅ Good: Short transaction
@Transactional
public void processOrder(Order order) {
orderRepository.save(order);
// Transaction ends here
}
public void sendNotifications(Order order) {
// Non-transactional
emailService.sendConfirmation(order);
shippingService.scheduleDelivery(order);
}
2. Use Read-Only for Queries
// ✅ Good: Read-only optimization
@Transactional(readOnly = true)
public List<User> getAllUsers() {
return userRepository.findAll();
}
Benefits:
- ⚡ Performance optimization
- 🔒 Prevents accidental modifications
- 📊 Database can optimize read-only queries
3. Set Appropriate Timeout
@Transactional(timeout = 5) // 5 seconds
public void quickOperation() {
// Must complete within 5 seconds
}
4. Handle Exceptions Properly
@Transactional(rollbackFor = Exception.class)
public void method() {
try {
// Business logic
} catch (SpecificException e) {
// Handle but still rollback
log.error("Error occurred", e);
throw e; // Re-throw to trigger rollback
}
}
5. Avoid Redundant Transaction Annotations
// ❌ Bad: Unnecessary nesting
@Transactional
public void methodA() {
methodB(); // Same transaction, unnecessary annotation
}
@Transactional // Redundant
public void methodB() {
// Uses same transaction as methodA
}
// ✅ Good: Only annotate entry point
@Transactional
public void methodA() {
methodB(); // No annotation needed
}
public void methodB() {
// Participates in methodA's transaction
}
Common Pitfalls
1. Self-Invocation Problem
@Service
public class UserService {
public void publicMethod() {
// Direct call - no proxy, no transaction!
this.transactionalMethod();
}
@Transactional
private void transactionalMethod() {
// Transaction NOT applied!
}
}
// ✅ Solution 1: Inject self
@Service
public class UserService {
@Autowired
private UserService self;
public void publicMethod() {
// Goes through proxy - transaction applied
self.transactionalMethod();
}
@Transactional
public void transactionalMethod() {
// Transaction applied
}
}
// ✅ Solution 2: Separate service
@Service
public class UserService {
@Autowired
private TransactionalUserService transactionalService;
public void publicMethod() {
transactionalService.transactionalMethod();
}
}
@Service
public class TransactionalUserService {
@Transactional
public void transactionalMethod() {
// Transaction applied
}
}
2. Catching Exceptions Without Re-throwing
// ❌ Bad: Exception swallowed
@Transactional
public void method() {
try {
// Business logic
} catch (Exception e) {
log.error("Error", e);
// Exception not re-thrown - transaction commits!
}
}
// ✅ Good: Re-throw exception
@Transactional
public void method() {
try {
// Business logic
} catch (Exception e) {
log.error("Error", e);
throw e; // Transaction rolls back
}
}
3. Wrong Isolation Level
// ❌ Bad: Too high isolation for simple query
@Transactional(isolation = Isolation.SERIALIZABLE)
public List<User> getUsers() {
return userRepository.findAll();
}
// ✅ Good: Appropriate isolation
@Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED)
public List<User> getUsers() {
return userRepository.findAll();
}
4. Checked Exceptions Don't Rollback by Default
// ❌ Bad: Checked exception doesn't rollback
@Transactional
public void method() throws IOException {
// Business logic
throw new IOException(); // Transaction commits!
}
// ✅ Good: Specify rollback for checked exceptions
@Transactional(rollbackFor = Exception.class)
public void method() throws IOException {
// Business logic
throw new IOException(); // Transaction rolls back
}
Conclusion
Spring Transaction Management is essential for maintaining data integrity in enterprise applications.
Key Takeaways
✅ Use declarative transactions (@Transactional) for clean code
✅ Understand propagation behaviors for complex scenarios
✅ Choose appropriate isolation levels based on requirements
✅ Keep transactions short to avoid performance issues
✅ Handle exceptions properly to ensure correct rollback
✅ Avoid common pitfalls like self-invocation and exception swallowing
✅ Use read-only transactions for queries
✅ Set timeouts to prevent long-running transactions
Quick Reference
// Most common pattern
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
rollbackFor = Exception.class,
timeout = 30
)
public void businessMethod() {
// Your business logic
}
Master these concepts, and you'll write robust, reliable transactional code in Spring applications!