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?
answer
- registerSynchronization needs active tx
- isSynchronizationActive() guard
- no tx → IllegalStateException
- getOrder() sorts within a phase (lower first)
- @TransactionalEventListener = declarative synchronization; drops event if no tx
basics
~20 sRegister 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 sYou 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// 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
Know registration needs a transaction and @TransactionalEventListener is the easy way to run after commit.
Explain the isSynchronizationActive() guard, IllegalStateException, and getOrder() within-phase ordering.
Map @TransactionalEventListener phases to the raw callbacks and know the no-transaction / fallbackExecution behavior.
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