When using @TransactionalEventListener with the default AFTER_COMMIT or AFTER_COMPLETION phase, any database writes in the listener method are being discard silently, e.g.:

@TransactionalEventListener
void onEvent(BookSavedEvent event) {
  // This does get called, but the following write to the database will be discarded silently
  var book = books.findById(event.bookId).orElseThrow();
  book.title = "New Title";
  books.save(book);
}

This problem doesn't happen if the phase is BEFORE_COMMIT or if just using the regular @EventListener instead.

I've made an example project using v2.5.0 demonstrating the problem: https://github.com/laech/spring-transactional-event-listener-disappearing-writes/blob/master/src/main/java/com/example/demo/DemoApplication.java

If you do ./gradlew bootRun on the above project you can see the mismatch in expectation.

Comment From: sbrannen

When your @TransactionalEventListener method is invoked after the transaction has already been committed, the transaction is still active for the current thread; however, you can no longer commit changes with that transaction.

Behind the scenes, the @TransactionalEventListener support is implemented via org.springframework.transaction.event.TransactionalApplicationListenerSynchronization. From the Javadoc for its afterCompletion() method, we see the following (from TransactionSynchronization.afterCompletion(int)):

Invoked after transaction commit/rollback. Can perform resource cleanup after transaction completion.

NOTE: The transaction will have been committed or rolled back already, but the transactional resources might still be active and accessible. As a consequence, any data access code triggered at this point will still "participate" in the original transaction, allowing to perform some cleanup (with no commit following anymore!), unless it explicitly declares that it needs to run in a separate transaction. Hence: Use PROPAGATION_REQUIRES_NEW for any transactional operation that is called from here.

The key point there is "(with no commit following anymore!)".

To use PROPAGATION_REQUIRES_NEW in your use case, you have to construct a TransactionTemplate as in the following revised implementation of your BookSavedListener.

@Component
static class BookSavedListener {

    @Autowired
    private BookRepository books;

    @Autowired
    private PlatformTransactionManager txManager;

    @TransactionalEventListener
    void onEvent(BookSavedEvent event) {
        TransactionDefinition txDefinition = new DefaultTransactionDefinition(PROPAGATION_REQUIRES_NEW);
        TransactionTemplate txTemplate = new TransactionTemplate(txManager, txDefinition);
        txTemplate.executeWithoutResult(status -> {
            var book = books.findById(event.bookId).orElseThrow();
            book.title = "New Title";
            books.save(book);
            System.out.println();
            System.out.println(books.findAll() + " <- expected");
        });
    }
}

With the above change, the updated book is in fact saved with the "New Title".

I am therefore closing this issue as "works as designed".

Comment From: sbrannen

Although the information necessary to understand this behavior is documented in the Javadoc for TransactionSynchronization#afterCompletion(int) and cross referenced from each enum constant in TransactionPhase, I think we can make this information more easily discoverable by mentioning it directly in TransactionPhase.

I am therefore reopening this issue to improve the documentation in TransactionPhase.

Comment From: laech

I think that documentation will need to be highlighted on TransactionalEventListener itself to make it as obvious as possible.

Ideally the framework should throw an exception in this case if this is an invalid operation, accepting the writes and then silently drops the them is pretty surprising behavior and will be/is the source of obscure bugs.

Comment From: sbrannen

I think that documentation will need to be highlighted on TransactionalEventListener itself to make it as obvious as possible.

In 1e1045ba427001c471bed10ac7ddc56f028a6301, I improved the documentation in @TransactionalEventListener as well as in TransactionPhase.