Explain how suspension and resumption of TransactionSynchronizations works. If code inside an outer transaction starts an inner Propagation.REQUIRES_NEW transaction, which synchronizations fire, and when?
answer
- synchronizations are per-transaction + thread-bound
- REQUIRES_NEW suspends: suspend() + clear + fresh empty set
- inner has its own synchronizations only
- resume(): rebind outer + call resume()
- outer afterCommit fires on OUTER commit, not inner's
basics
~20 sSynchronizations belong to one specific transaction. When a REQUIRES_NEW inner transaction suspends the outer one, Spring calls suspend() on the outer's synchronizations and unbinds them; the inner transaction gets its own empty set. When the inner one finishes, Spring rebinds the outer's and calls resume(). Each set fires on its own transaction's commit/rollback.
solid answer
~50 sTransactionSynchronizations are bound to the thread and belong to exactly one transaction. Propagation.REQUIRES_NEW suspends the outer transaction to run the inner one on a separate physical transaction. During suspension Spring's TransactionSynchronizationManager invokes suspend() on every active outer synchronization, records and clears them from the thread, and starts the inner transaction with a fresh, empty synchronization list. So a synchronization registered in the outer scope does NOT see or fire for the inner transaction — the inner transaction commits or rolls back independently, running only its own synchronizations' callbacks. When the inner transaction completes and control returns, Spring rebinds the saved outer synchronizations and calls resume() on each, and they later fire on the outer transaction's own commit/rollback. The takeaway: an afterCommit registered outside runs when the OUTER commits, even though an inner REQUIRES_NEW committed earlier; the two lifecycles are fully separate.
code
java · 24 lines@Service
class OrderService {
private final AuditService audit;
@Transactional // OUTER transaction
public void placeOrder(Order o) {
orderRepo.save(o);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override public void suspend() { /* unbind my resource, if any */ }
@Override public void resume() { /* rebind it */ }
@Override public void afterCommit() {
// fires on the OUTER commit, not when the inner audit committed
metrics.increment("orders.placed");
}
});
audit.log(o.getId()); // REQUIRES_NEW -> suspends OUTER + its synchronizations
}
}
@Service
class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW) // INNER, own synch set
public void log(Long id) { auditRepo.save(new Audit(id)); }
}go deeper
Know REQUIRES_NEW makes a separate transaction that commits independently.
Understand each transaction has its own synchronizations and the inner one doesn't fire the outer's callbacks.
Describe the suspend()→clear→fresh-set→resume() sequence and why outer afterCommit waits for the outer commit.
Reason about resource rebinding via suspend()/resume(), independent durability of nested transactions, and how @TransactionalEventListener binds to a specific transaction.
## Mental model: synchronizations are per-transaction, thread-bound Spring tracks the active synchronizations for the current transaction in a thread-local inside `TransactionSynchronizationManager` (a `Set<TransactionSynchronization>` plus flags like the active/name/read-only/isolation state). Everything you register belongs to *the transaction that is active on this thread right now*. ## What suspension is Certain propagations run a *different* physical transaction while an outer one is in progress: - **`Propagation.REQUIRES_NEW`** always suspends any existing transaction and starts a brand-new one. - **`Propagation.NOT_SUPPORTED`** suspends and runs non-transactionally. Suspension means: temporarily detach the outer transaction's state from the thread so the inner work has a clean slate, then re-attach it afterward. ## The suspend step When `AbstractPlatformTransactionManager` suspends a transaction it calls `TransactionSynchronizationManager.doSuspendSynchronization`, which: 1. Reads all currently registered synchronizations for the outer transaction. 2. Calls **`suspend()`** on each of them (giving them a chance to unbind their own external resources from the thread). 3. Clears the thread-local synchronization set (and captures the outer's name/read-only/isolation/active state into a `SuspendedResourcesHolder`). Now the thread has *no* active synchronizations. The inner `REQUIRES_NEW` transaction then initializes fresh synchronization (an empty set). Anything registered during the inner transaction lives only in that inner set. ## The inner transaction runs fully independently The inner transaction goes through its own complete lifecycle — including its own `beforeCommit`/`beforeCompletion`/`afterCommit`/`afterCompletion` — using only the synchronizations registered *inside* it. The outer synchronizations are dormant and do not fire. ## The resume step When the inner transaction finishes (commit or rollback) and Spring resumes the outer, `doResumeSynchronization`: 1. Re-initializes synchronization on the thread. 2. Re-registers each saved outer synchronization. 3. Calls **`resume()`** on each (letting them rebind resources to the thread). The outer transaction then continues as if nothing happened, and its synchronizations will fire later, at the *outer's* own completion. ## Concrete example ``` @Transactional // OUTER placeOrder(): register(syncA) // belongs to OUTER auditService.log() // @Transactional(REQUIRES_NEW) -> INNER suspend(): syncA.suspend(); outer synchs cleared INNER starts with empty synch set register(syncB) // belongs to INNER INNER commits: syncB.beforeCommit/afterCommit/afterCompletion fire resume(): outer synchs rebound; syncA.resume() ... outer continues ... OUTER commits: syncA.beforeCommit/afterCommit/afterCompletion fire ``` So `syncA.afterCommit` runs on the OUTER commit — *after* the inner already committed. If the outer later rolls back, `syncA` sees a rollback even though the inner audit row is already durably committed (independent lifecycles). ## Why suspend()/resume() exist on the interface Most domain synchronizations leave `suspend()`/`resume()` as no-ops. They matter for synchronizations that bind their *own* resource to the thread (Spring's own resource-managing synchronizations do this): on suspend they must unbind so the inner transaction does not accidentally use the outer's resource; on resume they rebind. This keeps thread-bound resources correctly scoped per transaction. ## Practical consequences / gotchas - A post-commit action registered in the outer scope does **not** fire when an inner `REQUIRES_NEW` commits — only when the outer commits. If you need 'after the inner commit', register inside the inner scope. - Because the inner transaction commits independently, its side effects are durable even if the outer rolls back — a deliberate REQUIRES_NEW property (useful for audit/logging that must survive a rollback), but a trap if you assumed one atomic unit. - `@TransactionalEventListener` bound to the outer transaction fires on the outer's phase, not the inner's. ## When this matters in design Use REQUIRES_NEW + inner synchronization when you *want* independent durability (audit trails, best-effort logging). Keep post-commit domain effects in the transaction whose commit actually represents 'the business fact happened'.
- If I register an afterCommit in the outer method, does it fire when the inner REQUIRES_NEW transaction commits?No. It's bound to the outer transaction, which is suspended during the inner one. It fires only when the outer transaction itself commits.
- What are suspend() and resume() actually for on the TransactionSynchronization interface?They let a synchronization unbind/rebind its own thread-bound resources across a suspension, so a nested REQUIRES_NEW transaction doesn't accidentally reuse the suspended transaction's resource. Most domain synchronizations leave them as no-ops.
saying these in an interview costs you the question
- Thinking outer synchronizations fire on an inner REQUIRES_NEW commit
- Believing the inner transaction shares the outer's synchronization set
- Assuming an inner REQUIRES_NEW commit rolls back if the outer later rolls back
- Treating suspend()/resume() as callbacks about commit/rollback rather than resource rebinding