Transaction Management in Spring

Spring supports declarative (@Transactional) and programmatic (TransactionTemplate) transactions. Declarative uses AOP: the framework starts a transaction before the method and commits or rolls back after. This article covers propagation, isolation, common pitfalls, and best practices with a reference table.

Overview

  • Declarative: @Transactional on class or method. Spring creates a proxy that starts a transaction before the method, commits on success, and rolls back on exception. By default rolls back only on RuntimeException and Error; checked exceptions do not trigger rollback—use rollbackFor.
  • Propagation: REQUIRED (join or create), REQUIRES_NEW (create new, suspend current), NESTED (savepoint), SUPPORTS, NOT_SUPPORTED, MANDATORY, NEVER. Controls how the method participates in an existing transaction.
  • Isolation: READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE. Default follows the database; you can override per method.
  • Self-invocation: @Transactional works via proxy. A method in the same class calling another @Transactional method does not go through the proxy, so no new transaction is started. Use a proxy reference or a separate bean.

Example

Example 1: Basic usage

Java
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order o) {
    orderRepo.save(o);
    inventoryRepo.deduct(o.getItems());
}
  • All exceptions (including checked) trigger rollback. Without rollbackFor, only RuntimeException and Error roll back.
  • Keep the method focused; avoid long-running logic, RPC, or file I/O inside the transaction.

Example 2: Propagation — REQUIRES_NEW

Java
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAudit(Audit a) {
    auditRepo.save(a);  // Independent transaction; outer rollback does not affect this
}
  • Use when you need an independent transaction (e.g. audit log that must persist even if the main business tx fails). Suspends the current transaction and creates a new one.

Example 3: Propagation — NESTED

Java
@Transactional(propagation = Propagation.NESTED)
public void updateInventory(Item item) {
    inventoryRepo.save(item);  // Uses savepoint; outer can roll back to before this
}
  • NESTED uses a savepoint when there is an existing transaction. Outer rollback rolls back the nested work. Requires JDBC savepoint support.

Example 4: Common pitfalls

IssueCauseFix
Tx not appliedSelf-invocation, non-public, proxy not appliedCall via proxy, use public, check AOP
Unexpected rollbackChecked exception not rolled back by defaultrollbackFor = Exception.class
Long transactionToo much logic, long durationSplit into smaller txs, use async
No rollback on catchSwallowing exceptionRe-throw or use TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()

Example 5: Programmatic transactions

Java
@Autowired TransactionTemplate txTemplate;

public void doWork() {
    txTemplate.execute(status -> {
        // Runs in transaction
        orderRepo.save(order);
        return null;
    });
}
  • Use when you need fine-grained control (e.g. conditional tx, multiple tx blocks in one method). TransactionTemplate handles begin/commit/rollback.

Example 6: Read-only transactions

Java
@Transactional(readOnly = true)
public User getById(Long id) {
    return userRepo.findById(id).orElseThrow();
}
  • readOnly=true hints the DB and frameworks (e.g. Hibernate) that no writes occur. Can enable optimizations; some DBs allow read replicas for read-only txs.

Core Mechanism / Behavior

  • Proxy: @Transactional uses AOP. The proxy intercepts the call, starts a transaction (or joins), invokes the target, then commits or rolls back. Self-invocation (this.method()) bypasses the proxy.
  • Propagation: REQUIRED joins an existing transaction or creates one. REQUIRES_NEW always creates a new transaction and suspends the current one. SUPPORTS runs in a tx if one exists, otherwise without. MANDATORY requires an existing tx; NEVER requires no tx.
  • Rollback: Default rollback on RuntimeException and Error. rollbackFor = Exception.class extends to all exceptions. noRollbackFor excludes specific exceptions from rollback.
  • PlatformTransactionManager: Abstraction over the actual transaction API (JDBC, JPA, JTA). Spring Boot auto-configures based on classpath.

Transaction Isolation in Practice

  • READ_COMMITTED: Default for many DBs. Prevents dirty reads; allows non-repeatable reads and phantom reads.
  • REPEATABLE_READ: Prevents dirty and non-repeatable reads; may still have phantoms (depending on DB).
  • SERIALIZABLE: Strongest; prevents all anomalies; highest cost. Use sparingly.
  • Override only when you understand the anomaly you are preventing; incorrect isolation can cause deadlocks or performance issues.

What to Avoid Inside a Transaction

Do not perform long-running or blocking operations inside a transactional method. Remote calls (RPC, HTTP) can hang or timeout, holding the transaction and database connection. File I/O and heavy computation delay commit and increase lock duration. If you must call external services, consider doing so after the transaction commits, or use a pattern that separates the transactional work from the external call. Similarly, avoid starting new threads that use the same transactional resources, as thread boundaries and transaction propagation can be complex and error-prone. When in doubt, keep the transactional method focused on database operations only.

Propagation in Depth

REQUIRED is the default: join the current transaction if one exists, otherwise create a new one. Most business methods use REQUIRED so they participate in a single logical transaction. REQUIRES_NEW creates an independent transaction every time; use it for operations that must commit even when the outer transaction fails, such as audit logging or sending a notification. SUPPORTS runs in a transaction if one exists, otherwise without; use for read-only operations that can work either way. MANDATORY requires an existing transaction and throws if there is none; use when the method must always run within a caller's transaction. NEVER throws if a transaction exists; use when the method must not run in a transaction. NOT_SUPPORTED suspends any existing transaction and runs without one; use when the method must not participate in the caller's transaction. NESTED uses a savepoint when a transaction exists, allowing partial rollback; not all databases support savepoints.

Key Rules

  • Keep transactions short; avoid RPC, file I/O, and long computation inside a tx.
  • Self-invocation does not trigger the proxy; use self-injection or a separate bean to hit the proxy.
  • rollbackFor as needed; use rollbackFor = Exception.class if all exceptions should roll back.
  • Read-only for query methods when appropriate; can improve performance.
  • Propagation: Understand REQUIRED vs REQUIRES_NEW; misuse can lead to unexpected commit/rollback behavior.

What's Next

See Spring AOP, MVCC, Distributed Transactions. See Database Transactions for isolation and anomaly details.