Transaction Propagation
Transaction Propagation
When one Spring-managed service method calls another, both annotated with @Transactional, Spring must decide: should the inner method join the existing transaction, suspend it and open a fresh one, or perhaps run without any transaction at all? This decision is controlled by the propagation attribute of @Transactional — one of the most practical and frequently misunderstood knobs in the entire Spring/JPA stack.
Understanding propagation lets you reason about database consistency, connection-pool usage, and subtle rollback behaviour that would otherwise produce mysterious bugs in production.
How Spring Implements Propagation
Spring wraps every @Transactional bean in a proxy. When a caller invokes a transactional method, the proxy intercepts the call and asks the PlatformTransactionManager to provide or suspend a transaction according to the configured propagation. The actual JDBC connection is bound to the current thread via TransactionSynchronizationManager. Once the annotated method returns (or throws), the proxy either commits or rolls back and releases the connection.
A and method B are in the same bean and A calls B directly (this.B()), Spring's proxy never intercepts the call and B's propagation setting is silently ignored. Always inject a reference to your own bean when you need intra-bean propagation semantics.
The Seven Propagation Levels
Spring defines propagation as the enum org.springframework.transaction.annotation.Propagation. Here are all seven values:
- REQUIRED (default) — join an existing transaction; start one if none exists.
- REQUIRES_NEW — always suspend any existing transaction and open a brand-new, independent one.
- NESTED — run inside a savepoint within the existing transaction; roll back only to the savepoint on failure, not the whole outer transaction.
- SUPPORTS — join if a transaction exists; run non-transactionally if not.
- NOT_SUPPORTED — always suspend any existing transaction and execute without one.
- MANDATORY — must join an existing transaction; throw
IllegalTransactionStateExceptionif there is none. - NEVER — must run without a transaction; throw if one exists.
REQUIRED — The Workhorse
REQUIRED is the default and the right choice for the vast majority of service methods. A single database connection is shared across the entire call stack, which means all writes participate in one atomic unit.
Key behaviour: if reduceStock throws an unchecked exception, the entire unit of work — including the save already called — is rolled back. That is usually exactly what you want.
REQUIRES_NEW — An Independent Transaction
REQUIRES_NEW tells Spring to suspend the caller's transaction, open a second independent connection, commit or roll back that connection, and then resume the original transaction. The two transactions are completely separate database sessions.
REQUIRES_NEW, the audit row is committed independently, so you never lose the trace of what was attempted.
REQUIRES_NEW holds two open JDBC connections simultaneously — one suspended, one active. With a small pool (e.g. HikariCP default 10), deeply nested or high-concurrency use of REQUIRES_NEW can exhaust the pool and cause thread starvation. Use it deliberately, not as a default.
NESTED — Savepoint-Based Partial Rollback
NESTED uses a JDBC savepoint to carve a sub-unit out of the existing transaction. If the inner method fails, Spring rolls back only to the savepoint — the outer transaction is still alive and can choose to commit or continue.
REQUIRES_NEW the inner work is already committed before the outer transaction finishes — it cannot be rolled back by the outer transaction. With NESTED, the inner work is uncommitted; if the outer transaction later rolls back, the inner savepoint is rolled back too.
SUPPORTS, NOT_SUPPORTED, MANDATORY, NEVER
These four are used less frequently but have clear purposes:
- SUPPORTS — read-heavy helper methods that work with or without a transaction. Useful for queries you expose in both transactional and non-transactional contexts.
- NOT_SUPPORTED — useful for long-running, read-only batch exports where holding a transaction open wastes a connection. Spring suspends any caller transaction and runs the method plain.
- MANDATORY — enforces that a method must always be called from within an active transaction. Great as a guard in lower-layer repository wrappers that should never be called without context.
- NEVER — enforces transactional cleanliness in methods that must not touch the database transactionally (e.g. async or background tasks launched via a scheduler).
Choosing the Right Propagation
A practical decision tree:
- Is the operation part of the same atomic unit as the caller? → REQUIRED (default).
- Must the operation commit regardless of what the caller does (e.g. audit, metrics)? → REQUIRES_NEW.
- Should partial failure be isolated without releasing the caller's connection? → NESTED.
- Is the method purely a read and you do not care about transactions? → SUPPORTS or add
readOnly = truewithREQUIRED. - Does calling without a transaction indicate a programming error? → MANDATORY.
Summary
Transaction propagation is the mechanism Spring uses to coordinate nested transactional calls. REQUIRED (default) shares a transaction; REQUIRES_NEW creates an independent, immediately committed unit; NESTED creates a savepoint for partial rollback within the outer transaction. Understanding these three covers the majority of real-world scenarios. In the next lesson you will add another dimension — isolation levels — which control what uncommitted data concurrent transactions can see.