skip to content

TransactionSynchronizationManager

TransactionSynchronizationManager holds the thread-bound resources and tells you whether a real transaction is active, and it is where you register lifecycle callbacks. Useful for framework-level code, and interviewers use it to check you know transactions live in a ThreadLocal.

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

questions

4

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

level: middleimportance: must knowfreq 55%

answer

  1. registerSynchronization + TransactionSynchronization
  2. afterCommit = commit only
  3. afterCompletion(status) = always
  4. guard with isSynchronizationActive()
  5. afterCommit runs AFTER commit, can't roll back

basics

~10 s

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

solid answer

~40 s

You 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 lines
java
import 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

for a junior

Know that you can register a callback to run after commit, and that afterCommit skips on rollback.

for a middle

Should write the registerSynchronization pattern, guard with isSynchronizationActive(), and pick afterCommit vs afterCompletion correctly.

for a senior

Explain that afterCommit runs post-commit outside the transaction and can't undo it; know the @TransactionalEventListener relationship.

for a principal

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

context

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

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

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