intermediate~2h

Propagation & Isolation in Spring

What happens when a @Transactional method calls another @Transactional method? Seven possible answers exist, and this is the chapter that makes all seven concrete instead of memorized.

◆ The problem

OrderService.placeOrder() is @Transactional and calls AuditService.logEvent(), which is also @Transactional. Is logEvent() part of the same transaction as placeOrder(), so that a failure in the order rolls back the audit log too? Or does it get its own, independent transaction? Propagation is Spring's answer — an explicit setting for exactly this question, rather than an implicit, surprising default.

TypeBehavior when called from within an existing transaction
REQUIRED (default)Joins the existing transaction — one shared unit of work; a failure anywhere rolls back everything.
REQUIRES_NEWSuspends the existing transaction, starts a brand new, fully independent one, then resumes the original after it completes.
NESTEDRuns within a SAVEPOINT (Chapter 06 §3) inside the existing transaction — can roll back just its own work without aborting the outer transaction.
SUPPORTSJoins an existing transaction if one exists; otherwise runs non-transactionally.
NOT_SUPPORTEDSuspends any existing transaction and runs entirely non-transactionally.
MANDATORYRequires an existing transaction to already be present — throws an exception if called outside one.
NEVERThrows an exception if called within an existing transaction — must be called standalone.
@Service public class OrderService { @Transactional // REQUIRED, the default public void placeOrder(OrderRequest request) { orderRepository.save(toOrder(request)); auditService.logEvent("ORDER_PLACED"); // see both variants below throw new RuntimeException("payment gateway timeout"); // simulate a late failure } } @Service public class AuditService { // Variant A: REQUIRED (default) — joins placeOrder()'s transaction @Transactional public void logEvent(String event) { auditRepository.save(new AuditLog(event)); // if placeOrder() later throws, THIS audit log entry is rolled back too — lost forever } // Variant B: REQUIRES_NEW — its own independent transaction @Transactional(propagation = Propagation.REQUIRES_NEW) public void logEventIndependently(String event) { auditRepository.save(new AuditLog(event)); // this COMMITS on its own, the instant this method returns — // survives even if placeOrder() rolls back afterward } }

This is exactly the real-world reason REQUIRES_NEW exists: an audit log entry, or a "payment attempt failed" record, is often something you explicitly want to survive even when the surrounding business transaction rolls back — the failure itself is the thing worth durably recording.

▲ Common mistake

REQUIRES_NEW suspends the outer transaction's database connection while the inner one runs on a separate connection — meaning the inner transaction cannot see the outer transaction's uncommitted writes (they're invisible to it, per normal isolation rules between separate transactions), and if the inner transaction locks a row the outer transaction also needs, you can deadlock against yourself. Use REQUIRES_NEW deliberately, not as a default reflex.

💻 Code example

@Service public class OrderService { @Transactional // REQUIRED, the default public void placeOrder(OrderRequest request) { orderRepository.save(toOrder(request)); auditService.logEvent("ORDER_PLACED"); // see both variants below throw new RuntimeException("payment gateway timeout"); // simulate a late failure } } @Service public class AuditService { // Variant A: REQUIRED (default) — joins placeOrder()'s transaction @Transactional public void logEvent(String event) { auditRepository.save(new AuditLog(event)); // if placeOrder() later throws, THIS audit log entry is rolled back too — lost forever } // Variant B: REQUIRES_NEW — its own independent transaction @Transactional(propagation = Propagation.REQUIRES_NEW) public void logEventIndependently(String event) { auditRepository.save(new AuditLog(event)); // this COMMITS on its own, the instant this method returns — // survives even if placeOrder() rolls back afterward } }
@Transactional(propagation = Propagation.NESTED) public void applyOptionalDiscount(Order order) { // runs inside a SAVEPOINT of the caller's transaction (Chapter 06 §3) if (!discountEngine.isEligible(order)) { throw new DiscountNotApplicableException(); // rolls back ONLY to the savepoint — the caller's transaction is NOT aborted, // and can continue and still commit everything else } }

◆ Under the hood

This is precisely why Chapter 06 §4 mattered: NESTED is not a second, independent database transaction the way REQUIRES_NEW is — it's literally implemented as a SAVEPOINT within the same single underlying database transaction. If the outer transaction eventually rolls back entirely, the nested work is rolled back with it (there's no independent commit to survive that); but if only the nested block fails, the outer transaction can catch that and continue, unaffected, resuming right after the savepoint. NESTED also requires the underlying JDBC driver to support savepoints — not every database/driver combination does.

💻 Code example

@Transactional(propagation = Propagation.NESTED) public void applyOptionalDiscount(Order order) { // runs inside a SAVEPOINT of the caller's transaction (Chapter 06 §3) if (!discountEngine.isEligible(order)) { throw new DiscountNotApplicableException(); // rolls back ONLY to the savepoint — the caller's transaction is NOT aborted, // and can continue and still commit everything else } }
@Transactional(isolation = Isolation.SERIALIZABLE, propagation = Propagation.REQUIRES_NEW) public void generateEndOfDayReport() { // directly maps to the SQL-level isolation level from Chapter 04 }

▲ Common mistake

Setting an isolation level on an inner method that's called via REQUIRED (joining an already-started outer transaction) has no effect — isolation level is fixed for the whole physical database transaction at the moment it actually begins, not per Java method call. Isolation level annotations only take effect on the method that actually starts the transaction (the outermost @Transactional call, or any method using REQUIRES_NEW).

💻 Code example

@Transactional(isolation = Isolation.SERIALIZABLE, propagation = Propagation.REQUIRES_NEW) public void generateEndOfDayReport() { // directly maps to the SQL-level isolation level from Chapter 04 }

Want a visual for this concept?

Generate a diagram tailored to “Propagation & Isolation in Spring” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to Transaction Manager Internals← Back to all Transaction Mastery chapters