Spring @Transactional Propagation: Choosing the Right Behavior for Your Use Case
Understand Spring's @Transactional propagation types (REQUIRED, REQUIRES_NEW, NESTED) with a worked payment-gateway example, rollback rules, read-only hints, and common mistakes like self-invocation and connection exhaustion.
24 Feb 2026, 13:53 UTC

The Problem and the Takeaway
Spring's @Transactional annotation hides complex transaction boundary decisions behind a simple attribute. The default Propagation.REQUIRED works for many cases, but it can cause unexpected rollbacks when a nested operation fails, or it can leave external side effects (like payment calls) in an inconsistent state when the caller later rolls back. The useful takeaway: match the propagation type to the business requirement—use REQUIRES_NEW for independent audit records, NESTED for partial rollbacks within a single logical transaction, and keep the default only when the whole method must succeed or fail as one unit.
Propagation Types and Their Mechanics
Spring defines seven propagation types. The three most commonly used in Spring Data JPA and plain JDBC are:
| Type | Behavior | Typical Use Case |
|---|---|---|
REQUIRED (default) | Joins an existing transaction; creates a new one if none exists. | Standard service-layer methods that should participate in the caller's transaction. |
REQUIRES_NEW | Suspends any current transaction and starts a fresh one with independent commit/rollback. | Logging, audit trails, or external gateway calls that must persist even if the caller rolls back. |
NESTED | Creates a JDBC 3.0 savepoint within the current transaction; the nested scope can roll back independently while the outer transaction stays active. | Batch processing where a single item failure should not abort the entire batch. |
NESTED requires a transaction manager that implements NestedTransactionAware (e.g., DataSourceTransactionManager or JpaTransactionManager on a savepoint-capable DataSource). JtaTransactionManager often lacks vendor support for savepoints and will throw NestedTransactionNotSupportedException.
Worked Example: Payment Gateway Audit Record
The following service method calls an external payment provider. The business rule: the audit record must be saved even if the caller later rolls back (e.g., due to a downstream inventory failure).
@Service
@RequiredArgsConstructor
public class PaymentService {
private final PaymentGatewayClient gateway;
private final PaymentAuditRepository auditRepo;
@Transactional(propagation = Propagation.REQUIRES_NEW,
rollbackFor = {OptimisticLockingFailureException.class,
CustomBusinessException.class})
public PaymentAuditRecord processPayment(PaymentRequest request) {
PaymentResponse response = gateway.charge(request);
PaymentAuditRecord audit = new PaymentAuditRecord(request, response);
return auditRepo.save(audit);
}
}Key points:
REQUIRES_NEWsuspends the caller's transaction, so the audit commit is independent.rollbackForlists only the exceptions that should abort the payment audit (optimistic locking and a custom business exception). Other checked exceptions will not trigger rollback, allowing the audit to commit.- The method is
publicon a Spring-managed bean—required for proxy interception.
Read-Only Optimization and Its Limits
@Transactional(readOnly = true) hints the persistence provider (Hibernate) to set flush mode to MANUAL and skip dirty checks. It does not enforce read-only at the database level unless the connection is explicitly configured as read-only. A method annotated readOnly = true that performs writes will still execute those writes, but may cause unexpected flush behavior or optimistic locking failures because the session assumes no changes.
Common Mistakes and How to Avoid Them
1. Self-Invocation Bypasses the Proxy
Calling a @Transactional method from another method in the same class bypasses the AOP proxy because the call is direct, not through the proxy.
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) { ... }
public void processOrder(Order order) {
createOrder(order); // No transaction boundary applied!
}
}Fixes:
- Extract the transactional method to a separate bean (preferred).
- Or enable class-based proxies:
@EnableAspectJAutoProxy(proxyTargetClass = true)(Spring Boot 3 defaults to this).
2. Private or Package-Private Methods Are Not Intercepted
Spring's JDK dynamic proxies only intercept public methods on interfaces. With proxyTargetClass = true (CGLIB), public and protected methods on classes are intercepted, but private methods never are. Keep transactional methods public.
3. REQUIRES_NEW in a Loop Exhausts Connections
Each iteration creates a new transaction that remains open until the outer method returns. In a long loop this can saturate the connection pool. Move the loop inside the transactional method or batch the work.
4. Overly Broad rollbackFor
Using rollbackFor = Exception.class catches control-flow exceptions like TransactionSystemException, masking infrastructure errors. List only the specific business exceptions that should trigger rollback.
5. Transaction Timeout May Be Ignored
The timeout attribute (seconds) is only honored by the underlying transaction manager. Many JTA providers ignore it. Verify with your specific provider.
Practical Verification Steps
- Enable DEBUG logging for
org.springframework.transactionto see messages likeCreating new transaction with name [...],Participating in existing transaction,Suspending current transaction. - Use
TransactionSynchronizationManagerin a debugger breakpoint:isActualTransactionActive()andgetCurrentTransactionName()confirm boundaries. - Write an integration test with
@SpringBootTestand@Transactionalon the test class. InjectJdbcTemplate, insert a row, call aNESTEDmethod that throws, and assert the outer row persists. - Check generated SQL with
spring.jpa.show-sql=trueto observe flush mode changes whenreadOnly = trueis applied.
Limitations
NESTEDpropagation is not portable across all transaction managers; test with your target database and transaction manager.REQUIRES_NEWcreates a physical transaction suspension; the outer transaction's locks are held until the inner transaction completes, increasing deadlock risk.- Spring's proxy-based AOP only works on Spring-managed beans. Objects created with
newor via non-Spring factories will not have transactional behavior.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.