๐Ÿงพ Spring @Transactional โ€” Rollback Rules, Propagation, and Read-Only Patterns

A hands-on reference for Spring and Jakarta/JTA transaction management.
Focuses on what actually bites developers in production: rollback semantics, propagation chains, and read-only optimizations.


๐Ÿงฉ 1. The essence of @Transactional

Transactions wrap a series of DB operations into an atomic unit โ€” all succeed or all fail.

@Transactional
public void process() {
  repo.save(...);
  repo.update(...);
  if (somethingWrong()) throw new RuntimeException();
}

If an unchecked exception occurs, Spring marks the transaction for rollback.


Default rules

Type of Exception Spring Default Jakarta Default Fix / Override
RuntimeException / Error rollback rollback โ€”
Checked Exception commit commit add rollbackFor=Exception.class (Spring) or rollbackOn=Exception.class (Jakarta)
Caught exception commit commit call setRollbackOnly() manually

โš™๏ธ 2. Rollback in practice

A) Unchecked exception โ†’ rollback automatically

@Transactional
public void placeOrder(Long userId) {
  orderRepo.save(new Order(...));
  paymentRepo.save(new Payment(...));
  throw new IllegalStateException("payment gateway down"); // rollback
}

Both inserts roll back.


B) Checked exception โ†’ commits unless configured

@Transactional
public void placeOrderChecked(Long userId) throws IOException {
  orderRepo.save(...);
  throw new IOException("printer failed"); // commits
}

C) Fix: include checked in rollback

@Transactional(rollbackFor = Exception.class)
public void placeOrderCheckedRollback(Long userId) throws IOException {
  orderRepo.save(...);
  throw new IOException("printer failed"); // rolls back
}

D) Jakarta equivalent

import jakarta.transaction.Transactional;

@Transactional(rollbackOn = Exception.class)
public void placeOrderJakarta(Long userId) throws IOException { ... }

๐Ÿ”„ 3. Propagation behavior

Each transaction can join, suspend, or create a new one.

Propagation Meaning Typical Use
REQUIRED (default) Join if exists, else start new Normal service calls
REQUIRES_NEW Suspend current, start new Auditing/logging
NESTED Create savepoint (rollback inner only) Partial rollback inside parent
SUPPORTS Run within tx if one exists Read-only operations
NOT_SUPPORTED Run non-transactionally Reporting
NEVER Throw error if a tx exists Safety guard

Example

@Transactional
public void placeOrder() {
  saveOrder();           // same tx
  audit_requiresNew();   // separate tx
  throw new RuntimeException("outer fails");
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void audit_requiresNew() {
  auditRepo.save(new AuditLog("order saved"));
}

Audit commits, even though the outer one rolls back.


๐Ÿ“˜ 4. Read-only transactions

@Transactional(readOnly = true)
public List<Order> listRecentOrders(int limit) {
  return orderRepo.findTopNByOrderByCreatedAtDesc(limit);
}

Hints ORM and drivers that no writes will occur. Improves performance (skips dirty checking, can use read-only DB replicas).


๐Ÿงฎ 5. Isolation levels and timeout

Spring-only attributes:

@Transactional(
  isolation = Isolation.REPEATABLE_READ,
  timeout = 10,
  readOnly = true
)
public List<User> queryActiveUsers() { ... }
  • Isolation.READ_COMMITTED (default) โ†’ prevents dirty reads.
  • Isolation.REPEATABLE_READ โ†’ consistent view of rows.
  • Isolation.SERIALIZABLE โ†’ full locking, slowest but safest.
  • timeout (seconds) โ†’ rollback if exceeded.

Jakarta/JTA @Transactional lacks these attributes โ€” you configure them via the datasource.


๐Ÿงฐ 6. Catching exceptions & marking rollback

If you catch an exception, the framework assumes everythingโ€™s fine โ€” it will commit unless you mark rollback manually.

import org.springframework.transaction.interceptor.TransactionAspectSupport;

@Transactional
public void catchButRollback() {
  try {
    doWork();
  } catch (Exception e) {
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  }
}

Jakarta equivalent:

import jakarta.transaction.TransactionSynchronizationRegistry;
import jakarta.annotation.Resource;

@Resource
TransactionSynchronizationRegistry tsr;

@jakarta.transaction.Transactional
public void catchButRollbackJta() {
  try {
    doWork();
  } catch (Exception e) {
    tsr.setRollbackOnly();
  }
}

๐Ÿงฑ 7. Self-invocation pitfall (proxy mechanics)

@Service
public class MyService {
  @Transactional
  public void outer() {
    inner(); // self-call bypasses proxy โ€” no tx applied
  }

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void inner() { ... }
}

Spring proxies intercept external calls only. Solutions:

  • Call via another bean (otherBean.inner()).
  • Use interface-based proxy injection.
  • Or enable AspectJ mode (@EnableAspectJAutoProxy(exposeProxy = true)).

โš–๏ธ 8. Choosing between Spring & Jakarta

Feature Spring @Transactional Jakarta/JTA @Transactional
isolation, timeout, readOnly โœ… yes โŒ no
rollbackFor, noRollbackFor โœ… yes โŒ no
propagation (REQUIRES_NEW, NESTED) โœ… yes limited (TxType.*)
Multiple transaction managers โœ… yes โŒ global TM only
XA / JTA compatibility โœ… via JtaTransactionManager โœ… native
Simplicity / portability moderate lightweight

Rule of thumb: Use Spring if you rely on JPA, multiple datasources, or fine-grained control. Use Jakarta if youโ€™re targeting EE containers or minimal portability.


๐Ÿงญ 9. Targeting specific transaction managers

@Transactional(transactionManager = "ordersTxManager")
public void writeOrder() { ... }

@Transactional(transactionManager = "billingTxManager")
public void writeInvoice() { ... }

Used when your app has multiple datasources or distinct persistence contexts.


๐Ÿ” 10. Common pitfalls checklist

โ˜‘ Rollback only happens if the framework sees the exception. โ˜‘ Checked exceptions need rollbackFor or rollbackOn. โ˜‘ Self-calls bypass proxy transactions. โ˜‘ readOnly=true is a hint, not a guarantee. โ˜‘ Use REQUIRES_NEW for independent commits (audits, logs). โ˜‘ Catching exceptions? Set rollback manually. โ˜‘ Pick one annotation style consistently. โ˜‘ Log transaction boundaries when debugging (DEBUG org.springframework.transaction).


๐Ÿงพ Summary Table

Concept Spring syntax Jakarta syntax Notes
Rollback on RuntimeException โœ”๏ธ (default) โœ”๏ธ (default) โ€”
Rollback on checked Exception rollbackFor=Exception.class rollbackOn=Exception.class must opt-in
Independent tx Propagation.REQUIRES_NEW TxType.REQUIRES_NEW inner commit survives outer failure
Read-only @Transactional(readOnly=true) โ€” optimization hint
Isolation/timeout attributes container config Spring feature
Nested tx Propagation.NESTED โ€” savepoint-based, JDBC only

๐Ÿš€ TL;DR Mental Model

  • A transaction begins at the first @Transactional boundary.
  • If an exception escapes the boundary, rollback occurs (runtime-only by default).
  • Nested calls usually share the same tx unless marked otherwise.
  • Caught exceptions commit unless you flag rollback manually.
  • Read-only methods are lighter, safer for queries.
  • Always log resolved propagation chains when debugging weird commits.

Outcome: You now have a complete transactional playbook โ€” clear rollback rules, predictable propagation, and a memory-safe mental map for every @Transactional boundary.