How do you run code reliably after a transaction commits, using TransactionSynchronizationManager?
answer
- registerSynchronization + TransactionSynchronization
- afterCommit = commit only
- afterCompletion(status) = always
- guard with isSynchronizationActive()
- afterCommit runs AFTER commit, can't roll back
basics
~10 sRegister a TransactionSynchronization callback via TransactionSynchronizationManager.registerSynchronization(...) and override afterCommit(). Spring invokes it only if the current transaction commits successfully.
solid answer
~40 sYou call `TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { ... })` from inside an active transaction and override the lifecycle method you need — most often `afterCommit()`, which fires only after a successful commit, or `afterCompletion(status)`, which always fires and tells you whether it committed or rolled back. This is the correct way to trigger side effects like sending an email, publishing to Kafka, or evicting a cache *only when the data was actually persisted*, avoiding the classic bug of firing them before commit and then rolling back. Registration requires synchronization to be active (check `isSynchronizationActive()`), otherwise it throws. In modern Spring you'd usually prefer the higher-level `@TransactionalEventListener(phase = AFTER_COMMIT)` or `TransactionSynchronization` helper methods, but both are built on exactly this mechanism.
code
java · 31 linesimport org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
@Service
class OrderService {
private final OrderRepository repo;
private final KafkaTemplate<String, OrderEvent> kafka;
OrderService(OrderRepository repo, KafkaTemplate<String, OrderEvent> kafka) {
this.repo = repo;
this.kafka = kafka;
}
@Transactional
public void placeOrder(Order order) {
repo.save(order); // may still roll back after this line
Runnable publish = () -> kafka.send("orders", new OrderEvent(order.getId()));
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override public void afterCommit() {
publish.run(); // only if commit succeeds
}
});
} else {
publish.run(); // no transaction: fire immediately
}
}
}go deeper
Know that you can register a callback to run after commit, and that afterCommit skips on rollback.
Should write the registerSynchronization pattern, guard with isSynchronizationActive(), and pick afterCommit vs afterCompletion correctly.
Explain that afterCommit runs post-commit outside the transaction and can't undo it; know the @TransactionalEventListener relationship.
Discuss delivery guarantees (at-most/at-least once), idempotency of post-commit side effects, and why this pattern is a stepping stone to outbox/CDC for true reliability.
## The problem it solves A very common bug: inside a `@Transactional` method you do `repository.save(order)` and then immediately `emailService.send(...)` or `kafkaTemplate.send(...)`. If the transaction rolls back *after* that line (constraint violation, later exception), you've already sent the email/event for data that was never persisted. You want the side effect to fire **only if and when the transaction commits**. ## TransactionSynchronization `TransactionSynchronization` (in `org.springframework.transaction.support`) is a callback interface with default (no-op) methods for each transaction lifecycle stage: - `beforeCommit(boolean readOnly)` — before the commit; you can still access the transactional resource / flush. - `beforeCompletion()` — before commit or rollback finalizes; resource cleanup point. - `afterCommit()` — **after a successful commit only**. Not called on rollback. - `afterCompletion(int status)` — **always** called, exactly once, after the transaction finishes. `status` is one of `STATUS_COMMITTED`, `STATUS_ROLLED_BACK`, or `STATUS_UNKNOWN`. - `flush()`, `suspend()`, `resume()` — for resource suspension/flush hooks. ## Registering ```java if (TransactionSynchronizationManager.isSynchronizationActive()) { TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { @Override public void afterCommit() { emailService.sendConfirmation(orderId); } }); } ``` - You **must** register while synchronization is active, i.e. inside a transaction scope — otherwise `registerSynchronization` throws `IllegalStateException`. Guard with `isSynchronizationActive()` and provide a fallback (run the side effect immediately) when there's no transaction. - Callbacks are held in TSM's thread-local synchronization list and executed in registration order (respecting `Ordered`). ## Key gotchas 1. **afterCommit runs outside the transaction.** By the time it fires, the transaction is already committed. If your callback itself does DB work, it runs *without* a transaction (or needs a new one). Exceptions thrown from `afterCommit()` propagate to the caller of commit but **cannot roll back** the already-committed transaction. 2. **afterCompletion cleanup must not fail silently** — Spring catches and logs exceptions from `afterCompletion` rather than propagating disruptively. 3. **Read-only / no real transaction:** if you registered inside a scope with synchronization but no physical transaction, `afterCommit` still fires on the (empty) commit. 4. **Prefer the declarative form:** `@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)` publishes an application event and internally registers a `TransactionSynchronization` for you — cleaner and testable. Use raw `registerSynchronization` when you're in framework/utility code without an event bus, or need fine control. ## When to use which phase - Send email / publish external event / evict remote cache → `afterCommit()`. - Guaranteed cleanup regardless of outcome (release a lock, clear a thread-local) → `afterCompletion()`. - Last-chance validation or additional flush that can still veto the commit → `beforeCommit()`.
- What happens if you call registerSynchronization when no transaction/synchronization is active?It throws IllegalStateException. That's why you guard with isSynchronizationActive() and provide a non-transactional fallback path that runs the side effect directly.
- If afterCommit() throws an exception, does the transaction roll back?No. By afterCommit the commit already happened and is irreversible. The exception propagates from the commit call but cannot undo the committed data — so afterCommit work should be idempotent/retriable.
- How does @TransactionalEventListener relate to this?It's the declarative equivalent: it registers a TransactionSynchronization under the hood and fires the listener at the chosen phase (default AFTER_COMMIT). Same mechanism, less boilerplate.
saying these in an interview costs you the question
- Believing afterCommit can roll back the transaction
- Thinking afterCommit fires on rollback too (that's afterCompletion)
- Calling registerSynchronization without checking synchronization is active
- Doing DB writes in afterCommit and assuming they're in the same transaction