Transaction Management in Spring Boot 3
Introduction
Transactions keep multi-step database work atomic—transfer money, create order + line items, or update inventory without partial failure. Spring Boot integrates Spring's declarative @Transactional on services. This chapter covers usage, propagation, rollback rules, and common pitfalls that cause "transaction not working" bugs.
Prerequisites
- Spring Data JPA: Entities and Repositories or MyBatis Integration
- Basic understanding of MySQL transactions
Enable Transaction Management
Spring Boot auto-configures transaction management when JPA or JDBC is on the classpath. Annotate service classes or methods:
@Service
public class OrderService {
@Transactional
public void placeOrder(PlaceOrderCommand cmd) {
// multiple DB operations in one transaction
}
}Code explanation:
- Proxy wraps bean—
@Transactionalonprivatemethods does not work - Call
@Transactionalmethods from another bean, not same class (this.placeOrder()bypasses proxy)
Rollback Rules
By default, rollback on unchecked exceptions only:
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
accountRepository.debit(fromId, amount);
accountRepository.credit(toId, amount);
}Force rollback on checked exception:
@Transactional(rollbackFor = Exception.class)
public void importRows(List<Row> rows) throws IOException {
// ...
}Never swallow exceptions inside transactional method without rethrow—commit may proceed incorrectly.
readOnly Optimization
@Transactional(readOnly = true)
public List<User> listUsers() {
return userRepository.findAll();
}Hints Hibernate/JPA to skip flush and can route to read replica in advanced setups.
Propagation (Common Values)
| Propagation | Meaning |
|---|---|
| REQUIRED (default) | Join current transaction or create new |
| REQUIRES_NEW | Suspend current; always new transaction |
| NOT_SUPPORTED | Run non-transactional |
| NEVER | Fail if transaction exists |
Nested checkout example:
Audit commits even if outer transaction rolls back later—use deliberately.
Isolation Level
Override default DB isolation per method:
@Transactional(isolation = Isolation.READ_COMMITTED)
public void reportJob() {
// ...
}MySQL InnoDB default is REPEATABLE READ—see isolation chapter.
MyBatis + @Transactional
Same pattern on service layer:
@Service
public class UserService {
private final UserMapper userMapper;
@Transactional
public void createUserWithProfile(User user, UserProfile profile) {
userMapper.insertUser(user);
userMapper.insertProfile(profile);
}
}Ensure DataSourceTransactionManager is active—auto-configured with JDBC/MyBatis starter.
Transaction Failure Scenarios
Warning
Self-Invocation Bypasses Proxy
@Service
public class BadService {
public void outer() {
this.inner();
}
@Transactional
public void inner() { }
}inner() runs without transaction—extract to another @Service bean.
Other pitfalls:
| Pitfall | Fix |
|---|---|
| Exception caught and not rethrown | Rethrow or mark rollback-only |
| Wrong exception type for rollback | Use rollbackFor |
| Non-public method | Make public |
Testing without @Transactional on test | Use @Rollback test annotations |
Programmatic rollback:
TransactionStatus status = transactionManager.getTransaction(def);
try {
// work
transactionManager.commit(status);
} catch (RuntimeException ex) {
transactionManager.rollback(status);
throw ex;
}Rare in Boot apps—prefer declarative @Transactional.
FAQ
@Transactional on controller?
Put on service layer—keeps web thin and reusable.
Read-only write still happens?
readOnly=true is hint—buggy code can still write; enforce in code review.
Transaction not rolling back?
Check exception type, self-invocation, and whether catch block eats error.
Multiple datasources?
Need separate TransactionManager per DataSource—advanced.
@Modifying JPA query?
Add @Transactional on repository method or calling service.
Connection pool exhausted?
Long transactions hold connections—keep scope short.