What is the fallbackExecution attribute and when would you set it to true?
answer
- default false = skip when no tx
- true = run like @EventListener
- only affects no-transaction case
- cause of 'listener never fires'
- no effect when tx present
basics
~10 sfallbackExecution 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.
solid answer
~40 s@TransactionalEventListener normally requires an active transaction: it registers a synchronization callback and fires at the chosen phase. If the event is published when no transaction is bound, the default behavior (fallbackExecution=false) is to silently skip the handler. That's a frequent source of 'my listener never runs' confusion, especially in tests or code paths that aren't wrapped in @Transactional. Setting fallbackExecution=true tells Spring to invoke the listener immediately and synchronously when there's no transaction, effectively degrading to @EventListener semantics for that case. Use it when the side effect should still happen outside a transactional context — for example when the same handler serves both transactional and non-transactional callers. If a transaction is present, fallbackExecution has no effect; the phase logic applies as usual.
code
java · 9 lines@Component
class CacheInvalidator {
// Runs after commit when in a tx; runs immediately when there is no tx
@TransactionalEventListener(fallbackExecution = true)
public void onChanged(EntityChangedEvent e) {
cache.evict(e.key());
}
}go deeper
Knows default false means no run without a transaction.
Explains true degrades to immediate @EventListener behavior for the no-tx case.
Connects it to the 'listener never fires' bug and test rollback semantics.
Judges when fallback undermines the 'only-on-commit' guarantee and designs handlers accordingly.
## The attribute `fallbackExecution` is a boolean on `@TransactionalEventListener`, defaulting to **false**. ## Default behavior (false) The listener only works by registering a `TransactionSynchronization` against the **current** transaction. If, at `publishEvent(...)` time, **no transaction is active** (`TransactionSynchronizationManager.isSynchronizationActive()` is false), there is nothing to register against, so Spring **skips** the listener entirely and logs at debug/trace that no transaction is active. No exception, no invocation. This is the classic gotcha: a handler that works in production (where calls are `@Transactional`) does nothing in a unit test or a code path lacking a transaction. ## fallbackExecution = true When set to true, and no transaction is active, Spring invokes the listener **immediately and synchronously** at publish time — the same as a plain `@EventListener`. When a transaction **is** active, the flag is irrelevant and the normal phase-based deferral applies. ## When to use - The listener's side effect should still occur even when the caller isn't transactional (e.g., a shared handler used by both transactional service methods and non-transactional utility code). - You want deterministic behavior in tests without wrapping everything in transactions. ## When NOT to use - If the whole point is 'only do this if data committed', enabling fallback would run the side effect even when nothing was persisted — defeating the purpose. Keep it false there. ## Interaction with phase fallbackExecution does not change the phase; it only governs the **no-transaction** case. With a transaction present, BEFORE_COMMIT/AFTER_COMMIT/etc. behave normally. ## Testing tip In integration tests, if you want the transactional listener to actually fire on commit, don't run the test method itself in a rolled-back @Transactional (Spring test rolls back by default) — otherwise AFTER_COMMIT never fires. Either commit explicitly or use fallbackExecution / TestTransaction control depending on what you're verifying.
- You wrote a @TransactionalEventListener but it never fires in a unit test. Likely cause?The test path has no active transaction, so with the default fallbackExecution=false the listener is skipped. Wrap the publisher in a real (committing) transaction, or set fallbackExecution=true.
saying these in an interview costs you the question
- Thinking fallbackExecution changes the transaction phase
- Assuming the listener always runs even without a transaction by default
- Believing fallbackExecution=true still defers execution when there is no tx