skip to content

Synchronization & Transactional Events

Hooking into the transaction lifecycle: the thread-bound resource manager, synchronization callbacks, @TransactionalEventListener phases, and deferring side effects until after commit. Interviewers reach here when the topic turns to messaging that must not fire on a rollback.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

20

Why would you delay sending an email or publishing a message until after the database transaction commits, instead of doing it inline in your service method?

level: juniorimportance: must knowfreq 70%

answer

  1. rollback can't un-send an email
  2. side effects aren't transactional
  3. wait until data is durable
  4. AFTER_COMMIT = truthful notification
  5. crash-after-commit still loses it

basics

~20 s

Because the transaction might still roll back. If you email or publish before commit and the transaction then fails, you have acted on data that was never saved. Waiting until after commit ensures the side effect only happens for data that actually persisted.

solid answer

~40 s

A database transaction can still roll back after your code runs — a later step throws, a constraint fails, or commit itself errors. If you send an email or publish a Kafka message inline, that side effect already left the building and cannot be undone: you have notified about an order that does not exist. External side effects are not transactional, so you defer them to AFTER_COMMIT — the point where the data is guaranteed durable. In Spring you express this with an event plus @TransactionalEventListener(phase = AFTER_COMMIT), or by registering a TransactionSynchronization. The trade-off: if the app crashes after commit but before the side effect runs, the notification is lost — that residual gap is what the transactional outbox pattern later addresses.

code

java · 21 lines
java
// BAD: side effect fires before the transaction is guaranteed to commit
@Transactional
public void placeOrder(Order order) {
    orderRepository.save(order);
    emailService.sendConfirmation(order); // if a later step rolls back,
                                          // the email is already gone
    inventoryService.reserve(order);      // <-- may throw -> rollback
}

// GOOD: publish an event; deliver the email only AFTER_COMMIT
@Transactional
public void placeOrder(Order order) {
    orderRepository.save(order);
    inventoryService.reserve(order);
    events.publishEvent(new OrderPlaced(order.getId()));
}

@TransactionalEventListener // phase defaults to AFTER_COMMIT
public void onOrderPlaced(OrderPlaced e) {
    emailService.sendConfirmation(e.orderId());
}

go deeper

for a junior

Must grasp the core idea: a rollback cannot un-send an email, so wait until the data is committed.

for a middle

Should name the concrete Spring mechanism (@TransactionalEventListener AFTER_COMMIT) rather than describing it vaguely.

for a senior

Should articulate the residual crash-after-commit gap and gesture at the outbox pattern.

for a principal

Frames this as one instance of the dual-write consistency problem and knows the boundary of what AFTER_COMMIT can and cannot guarantee.

## The core problem A relational transaction gives you atomicity **for the database only**. Everything you do to the outside world — sending an email, publishing to Kafka/RabbitMQ, calling a REST API, writing a file — is **not** part of that transaction and **cannot be rolled back**. Consider a naive service: 1. `orderRepository.save(order)` 2. `emailService.sendConfirmation(order)` ← side effect fires here 3. ...more work that throws an exception Spring's declarative transaction (`@Transactional`) will now roll back step 1 — the order is **not** in the database — but the confirmation email in step 2 has **already been sent**. The customer got a receipt for an order that does not exist. This is a classic **premature side effect** bug. ## The fix: run side effects AFTER_COMMIT You move the side effect to a hook that Spring fires **only after the transaction has successfully committed**. At that moment the data is durably persisted, so any notification about it is truthful. Two idiomatic Spring mechanisms: 1. **`@TransactionalEventListener`** — publish a domain event during the transaction; a listener annotated `@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)` (AFTER_COMMIT is the default phase) runs the side effect after commit. This is the clean, decoupled approach. 2. **`TransactionSynchronizationManager.registerSynchronization(...)`** — a lower-level callback API where you override `afterCommit()`. ## What 'after commit' guarantees — and does not - **Guarantees:** the transaction reached the durable state. Nothing your side effect sees will later vanish through rollback. - **Does NOT guarantee delivery:** if the JVM crashes, or the broker is down, in the window *between* commit and the side effect completing, the side effect is simply lost. Commit happened; the email did not. Spring does not retry it for you. This narrower failure mode is the **dual-write problem**, solved separately by the **transactional outbox** pattern (persist the intent to send inside the same transaction, deliver it asynchronously from that table). ## When to use Use AFTER_COMMIT for **any non-transactional side effect that must reflect committed data**: notifications, message/event publication, cache invalidation of external caches, triggering downstream workflows. Do **not** use it for work that must be atomic with the data — that belongs inside the transaction. ## Junior-friendly rule of thumb > Anything you cannot un-send should wait until the data is safely saved.

  • If the transaction rolls back, does the AFTER_COMMIT listener still run?
    No. AFTER_COMMIT listeners only fire on a successful commit. On rollback they are skipped entirely (an AFTER_ROLLBACK phase exists separately if you need that).
  • Is the problem fully solved once you move the email to AFTER_COMMIT?
    Mostly, but not completely. You have eliminated false notifications on rollback. A residual gap remains: if the app crashes between commit and sending, the email is lost — that's the dual-write problem the outbox pattern addresses.

saying these in an interview costs you the question

  • Claiming the email can be 'rolled back' along with the database transaction — external side effects are not transactional.
  • Believing AFTER_COMMIT guarantees the side effect will definitely be delivered (it doesn't survive a crash after commit).
  • Thinking the listener runs even on rollback.

context

open as a page

What is @TransactionalEventListener and what is its default phase?

level: juniorimportance: must knowfreq 55%

basics

~10 s

It's a Spring event listener that only fires relative to a transaction. By default it runs in the AFTER_COMMIT phase, so it executes only after the surrounding transaction successfully commits.

open as a page

Explain how @TransactionalEventListener with TransactionPhase.AFTER_COMMIT works, and the gotchas around it (default phase, no active transaction, doing DB writes in the listener).

level: middleimportance: must knowfreq 65%

basics

~20 s

You publish an event inside the transaction; a method annotated @TransactionalEventListener runs it after the transaction commits (AFTER_COMMIT is the default phase). Gotchas: if there is no active transaction the event is dropped unless fallbackExecution=true, and DB writes in the listener need a new transaction.

open as a page

Explain the four TransactionPhase values (BEFORE_COMMIT, AFTER_COMMIT, AFTER_ROLLBACK, AFTER_COMPLETION) and when each fires.

level: middleimportance: must knowfreq 50%

basics

~10 s

BEFORE_COMMIT fires just before commit (still in the tx). AFTER_COMMIT (default) fires after a successful commit. AFTER_ROLLBACK fires after a rollback. AFTER_COMPLETION fires after the tx ends either way.

open as a page

How do you run code reliably after a transaction commits, using TransactionSynchronizationManager?

level: middleimportance: must knowfreq 55%

basics

~10 s

Register a TransactionSynchronization callback via TransactionSynchronizationManager.registerSynchronization(...) and override afterCommit(). Spring invokes it only if the current transaction commits successfully.

open as a page

You moved message publishing to AFTER_COMMIT so you never publish on rollback. A colleague says the system is now fully consistent. What failure can still occur, and what is this problem called?

level: seniorimportance: must knowfreq 55%

basics

~20 s

The DB can commit and then the app crash (or the broker be down) before the message is published, so the event is lost forever. AFTER_COMMIT prevents false events but not lost ones. This is the dual-write problem: two systems updated without a shared transaction.

open as a page

What is a TransactionSynchronization, and why would you use its afterCommit callback instead of just putting the code at the end of a @Transactional method?

level: juniorimportance: should knowfreq 42%

basics

~20 s

A TransactionSynchronization is a hook Spring calls at points in a transaction's life. Its afterCommit runs only after the transaction actually commits — so side effects like sending an email won't happen if the transaction rolls back.

open as a page

What is TransactionSynchronizationManager, and what does isActualTransactionActive() tell you?

level: juniorimportance: should knowfreq 35%

basics

~10 s

It's a Spring helper that stores transaction state per thread. isActualTransactionActive() returns true when a real database transaction is currently open on the calling thread.

open as a page

Besides @TransactionalEventListener, how can you hook a callback to run after the current transaction commits using the lower-level synchronization API? When would you reach for it?

level: middleimportance: should knowfreq 40%

basics

~10 s

Call TransactionSynchronizationManager.registerSynchronization(...) with a TransactionSynchronization whose afterCommit() method holds your side effect. Spring runs it after the current transaction commits. Use it for imperative, one-off deferrals where publishing a full domain event is overkill.

open as a page

Name the TransactionSynchronization callbacks and describe the exact order they fire in on a successful commit versus on a rollback.

level: middleimportance: should knowfreq 40%

basics

~10 s

Commit path: beforeCommit, then beforeCompletion, then the physical commit, then afterCommit, then afterCompletion(STATUS_COMMITTED). Rollback path: beforeCommit is skipped; beforeCompletion runs, then rollback, then afterCompletion(STATUS_ROLLED_BACK). afterCommit never runs on rollback.

open as a page

How do you register a TransactionSynchronization, what must be true for registration to succeed, how is invocation order among multiple synchronizations decided, and how does @TransactionalEventListener build on this?

level: middleimportance: should knowfreq 30%

basics

~20 s

Register via TransactionSynchronizationManager.registerSynchronization(sync), but only inside an active Spring transaction — otherwise it throws IllegalStateException. If you register several, they run in Ordered order (getOrder). @TransactionalEventListener registers such a synchronization for you from a published event.

open as a page

What is the fallbackExecution attribute and when would you set it to true?

level: middleimportance: should knowfreq 35%

basics

~10 s

fallbackExecution controls what happens when no transaction is active. Default false means the listener is skipped. Setting it true makes the listener run immediately (like a plain @EventListener) even without a transaction.

open as a page

What happens if you perform JPA/JDBC writes inside an afterCommit callback? Why do people say afterCommit runs 'outside' the transaction, and how do you write to the DB safely there?

level: seniorimportance: should knowfreq 34%

basics

~20 s

By afterCommit the original transaction is already committed and its resources are being cleaned up, so writes there are not part of that transaction. To write safely, open a fresh transaction (e.g. a REQUIRES_NEW @Transactional method) from inside the callback.

open as a page

How does an exception thrown from each of the four callbacks (beforeCommit, beforeCompletion, afterCommit, afterCompletion) affect the transaction outcome and the caller?

level: seniorimportance: should knowfreq 32%

basics

~10 s

beforeCommit throwing rolls the transaction back and propagates to the caller. beforeCompletion and afterCompletion exceptions are logged and swallowed. afterCommit exceptions propagate to the caller but cannot undo the already-committed transaction.

open as a page

Why do database writes performed inside an AFTER_COMMIT listener often get silently lost, and how do you fix it?

level: seniorimportance: should knowfreq 45%

basics

~10 s

The original transaction is already committed when AFTER_COMMIT runs, so there's no active writable transaction. Persistence there isn't flushed/committed. Fix it by starting a new transaction with @Transactional(propagation = REQUIRES_NEW).

open as a page

What do bindResource() and getResource() do, and how do they make one Connection/EntityManager shared across a transaction?

level: seniorimportance: should knowfreq 40%

basics

~20 s

They store and retrieve a resource (like a JDBC Connection) in a per-thread map keyed by its factory (the DataSource). Binding once lets all code in the transaction fetch the same resource instead of opening new ones.

open as a page

Design a reliable event-publishing mechanism that fixes the dual-write gap left by AFTER_COMMIT. Explain the transactional outbox pattern, its delivery semantics, and how it relates to Spring Modulith's event publication registry.

level: principalimportance: should knowfreq 45%

basics

~20 s

Write the event into an outbox table in the same transaction as the business data, so both commit atomically. A separate relay reads the outbox and publishes to the broker, marking rows done and retrying failures. This gives at-least-once delivery; consumers must be idempotent. Spring Modulith's event publication registry is an in-process version of this.

open as a page

Why does transaction context (and TransactionSynchronizationManager state) fail to carry over to @Async, executor, or reactive threads — and how do you handle it?

level: principalimportance: should knowfreq 35%

basics

~20 s

Because TSM stores everything in ThreadLocals tied to the calling thread. A new thread starts empty, so isActualTransactionActive() is false and no connection is bound. You must start a fresh transaction on that thread or hand off after commit.

open as a page

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?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

Synchronizations 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.

open as a page

How do @Async, exception handling, and durability interact with AFTER_COMMIT listeners, and where does the outbox pattern fit?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

By default the AFTER_COMMIT listener runs synchronously in the committing thread; adding @Async moves it to another thread. Exceptions after commit only get logged. If the process crashes after commit but before the side effect, the side effect is lost — the transactional outbox pattern closes that gap.

open as a page