skip to content

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%

answer

  1. registerSynchronization needs active tx
  2. isSynchronizationActive() guard
  3. no tx → IllegalStateException
  4. getOrder() sorts within a phase (lower first)
  5. @TransactionalEventListener = declarative synchronization; drops event if no tx

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.

solid answer

~40 s

You call TransactionSynchronizationManager.registerSynchronization(mySync) from within a Spring-managed transaction. The precondition is that synchronization is active on the thread — i.e., you're inside a @Transactional boundary; otherwise it throws IllegalStateException ('Transaction synchronization is not active'). You can guard with isSynchronizationActive(). When multiple synchronizations are registered, Spring sorts them with the Ordered contract (TransactionSynchronization has a getOrder() defaulting to LOWEST_PRECEDENCE), so lower order runs first within each phase. @TransactionalEventListener is the declarative layer: when you publish an ApplicationEvent inside a transaction, Spring registers an internal TransactionSynchronization adapter and dispatches your handler at the configured phase (AFTER_COMMIT by default, or BEFORE_COMMIT / AFTER_ROLLBACK / AFTER_COMPLETION). By default, if no transaction is active the event is dropped unless fallbackExecution = true.

code

java · 14 lines
java
// Low-level registration with a guard and explicit ordering
if (TransactionSynchronizationManager.isSynchronizationActive()) {
    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronization() {
            @Override public int getOrder() { return 10; } // lower = earlier
            @Override public void afterCommit() { broker.publish(event); }
        });
}

// Declarative equivalent — Spring registers the synchronization for you
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderPlaced(OrderPlaced e) {
    broker.publish(e);
}

go deeper

for a junior

Know registration needs a transaction and @TransactionalEventListener is the easy way to run after commit.

for a middle

Explain the isSynchronizationActive() guard, IllegalStateException, and getOrder() within-phase ordering.

for a senior

Map @TransactionalEventListener phases to the raw callbacks and know the no-transaction / fallbackExecution behavior.

for a principal

Choose between raw synchronizations and event listeners deliberately, and reason about deterministic ordering and the active-transaction vs active-synchronization distinction.

## Registering a synchronization ```java TransactionSynchronizationManager.registerSynchronization(mySync); ``` `TransactionSynchronizationManager` is a thread-local coordinator. `registerSynchronization` adds your `TransactionSynchronization` to the current transaction's set. ## Precondition: synchronization must be active The call only works when a Spring-managed transaction with **active synchronization** exists on the thread. Otherwise it throws `IllegalStateException: Transaction synchronization is not active`. In practice this means you must be inside a `@Transactional` method (or a programmatic `TransactionTemplate`). To register conditionally and avoid the exception when there might be no transaction: ```java if (TransactionSynchronizationManager.isSynchronizationActive()) { TransactionSynchronizationManager.registerSynchronization(mySync); } else { // no transaction: run inline, or skip, or throw a clearer error } ``` Note: an *active transaction* and *active synchronization* are subtly different — a `PlatformTransactionManager` normally initializes synchronization, but exotic configs can have a transaction without synchronization. `isSynchronizationActive()` is the correct guard for registration. ## Ordering among multiple synchronizations `TransactionSynchronization` extends `Ordered` (via `Ordered`/`OrderComparator`) with `getOrder()` defaulting to `Ordered.LOWEST_PRECEDENCE`. When Spring drives a phase (say, all `afterCommit` calls), it retrieves the synchronizations **sorted by order** (`OrderComparator.sort`), so **lower `getOrder()` runs first**. Every registered synchronization completes its `beforeCommit` before any `afterCommit` runs — ordering applies *within* each phase, not across phases. Override `getOrder()` when you need deterministic sequencing between independent synchronizations (e.g., publish-to-broker must run before metrics). ## How @TransactionalEventListener builds on this `@TransactionalEventListener` is the ergonomic, event-driven front end: ```java @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void on(OrderPlaced e) { publisher.publish(e); } ``` Mechanism: when you `applicationEventPublisher.publishEvent(new OrderPlaced(...))` inside a transaction, Spring's `TransactionalApplicationListenerMethodAdapter` registers an internal `TransactionSynchronization` on the current transaction. At the configured phase it invokes your method: - `BEFORE_COMMIT` → `beforeCommit` - `AFTER_COMMIT` (default) → `afterCommit` - `AFTER_ROLLBACK` → `afterCompletion` filtered to rolled-back - `AFTER_COMPLETION` → `afterCompletion` **No active transaction?** By default the event listener simply does **not** fire (the event is effectively dropped for the transactional listener). Set `fallbackExecution = true` to run it immediately in that case. You can also order these listeners with `@Order`. ## When to use which - Ad-hoc, low-level, or infrastructure hooks (resource cleanup, custom ordering, direct access to status) → implement `TransactionSynchronization` and register it. - Domain 'do X after commit' semantics with decoupling → publish an event + `@TransactionalEventListener`. It's the same underlying mechanism, but declarative and testable. ## Common gotchas - Registering outside a transaction → `IllegalStateException`; guard with `isSynchronizationActive()`. - Expecting cross-phase ordering from `getOrder()` — it only orders within a phase. - Forgetting that an `AFTER_COMMIT` event listener silently does nothing if there is no surrounding transaction (unless `fallbackExecution`).

  • What happens if an @TransactionalEventListener event is published with no active transaction?
    By default the listener does not fire (the event is dropped for that listener). Set fallbackExecution = true to run it immediately outside a transaction.
  • Does getOrder() control ordering across phases, e.g. one sync's beforeCommit vs another's afterCommit?
    No. Phases run in fixed sequence for all synchronizations; getOrder() only decides the relative order among synchronizations within the same phase.

saying these in an interview costs you the question

  • Thinking you can register a synchronization without any transaction
  • Assuming @TransactionalEventListener fires even with no transaction by default
  • Believing getOrder() reorders phases rather than within a phase
  • Confusing TransactionSynchronizationManager (thread-local coordinator) with the transaction manager bean

context