If the ApplicationEventMulticaster bean appears in the context, the @TransactionalEventListener annotation is useless, it will not work a priori and is not mentioned anywhere.

At the stage of this method, there is still a transaction in the publish event call chain, SimpleApplicationEventMulticaster.multicastEvent:

        @Override
    public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
        ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
        Executor executor = getTaskExecutor();
        for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
            if (executor != null) {
                executor.execute(() -> invokeListener(listener, event));
            }
            else {
                invokeListener(listener, event);
            }
        }
    }

If we have an ApplicationEventMulticaster bean, then the first if will be executed and invokeListener will be executed deferred in a separate thread WHERE OUR TRANSACTION WILL NOT BE ALREADY.

Here's what's happening in invokeListener -> doInvokeListener -> TransactionalApplicationListenerMethodAdapter.onApplicationEvent():

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (TransactionSynchronizationManager.isSynchronizationActive() &&
                TransactionSynchronizationManager.isActualTransactionActive()) {
            TransactionSynchronizationManager.registerSynchronization(
                    new TransactionalApplicationListenerSynchronization<>(event, this, this.callbacks));
        }
        else if (this.annotation.fallbackExecution()) {
            if (this.annotation.phase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) {
                logger.warn("Processing " + event + " as a fallback execution on AFTER_ROLLBACK phase");
            }
            processEvent(event);
        }
        else {
            // No transactional event execution at all
            if (logger.isDebugEnabled()) {
                logger.debug("No transaction is active - skipping " + event);
            }
        }
    }

When this code is executed asynchronously in a separate thread (executor), the transaction no longer exists.

Comment From: jhoeller

Related issue: #25129

We can mark TransactionalApplicationListener with an indicator that it needs to always execute in the caller's thread and react to that in SimpleApplicationEventMulticaster, bypassing a potential custom executor for such a listener there.