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

Enable Transaction Management

Spring Boot auto-configures transaction management when JPA or JDBC is on the classpath. Annotate service classes or methods:

java
@Service
public class OrderService {
 
    @Transactional
    public void placeOrder(PlaceOrderCommand cmd) {
        // multiple DB operations in one transaction
    }
}

Code explanation:

  • Proxy wraps bean—@Transactional on private methods does not work
  • Call @Transactional methods from another bean, not same class (this.placeOrder() bypasses proxy)

Rollback Rules

By default, rollback on unchecked exceptions only:

java
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
    accountRepository.debit(fromId, amount);
    accountRepository.credit(toId, amount);
}

Force rollback on checked exception:

java
@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

java
@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)

PropagationMeaning
REQUIRED (default)Join current transaction or create new
REQUIRES_NEWSuspend current; always new transaction
NOT_SUPPORTEDRun non-transactional
NEVERFail 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:

java
@Transactional(isolation = Isolation.READ_COMMITTED)
public void reportJob() {
    // ...
}

MySQL InnoDB default is REPEATABLE READ—see isolation chapter.

MyBatis + @Transactional

Same pattern on service layer:

java
@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

java
@Service
public class BadService {
    public void outer() {
        this.inner();
    }
    @Transactional
    public void inner() { }
}

inner() runs without transaction—extract to another @Service bean.

Other pitfalls:

PitfallFix
Exception caught and not rethrownRethrow or mark rollback-only
Wrong exception type for rollbackUse rollbackFor
Non-public methodMake public
Testing without @Transactional on testUse @Rollback test annotations

Programmatic rollback:

java
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.