java 107 lines · 4 tabs

Transactional Domain Events With Spring's ApplicationEventPublisher and @TransactionalEventListener

Shared by codesnips Jul 2026
4 tabs
package com.shop.orders.events;

import java.math.BigDecimal;
import java.time.Instant;

public record OrderPlacedEvent(
        Long orderId,
        String customerEmail,
        BigDecimal amount,
        Instant placedAt
) {
    public static OrderPlacedEvent from(Long orderId, String email, BigDecimal amount) {
        return new OrderPlacedEvent(orderId, email, amount, Instant.now());
    }
}
4 files · java Explain with highlit

This snippet shows the standard Spring way to decouple side effects from core business logic using in-process domain events. Instead of an OrderService directly calling an email sender and a search indexer, it publishes an event and lets independent listeners react. That keeps the write path focused on persistence and its invariants while cross-cutting concerns evolve separately.

In OrderPlacedEvent, the event is a plain immutable value object — a Java record carrying only the identifiers and data a listener needs (orderId, customerEmail, amount). Domain events should be self-contained facts about something that already happened, not references to mutable entities, so listeners never re-read half-committed state.

OrderService performs the real work: it saves the entity inside a @Transactional method and then calls events.publishEvent(...). A key detail is that ApplicationEventPublisher.publishEvent is synchronous by default, so without extra configuration a listener would run inside the same transaction and a failure there could roll back the order. Publishing after repository.save keeps the event tied to the committed intent.

OrderEventListeners is where the interesting behavior lives. The sendConfirmationEmail handler uses @TransactionalEventListener with the default AFTER_COMMIT phase, meaning it only fires once the surrounding transaction actually commits — the confirmation email is never sent for an order that rolled back. It is also annotated @Async so the mail call runs off the request thread and cannot slow down the caller. The updateReadModel handler demonstrates BEFORE_COMMIT, useful when a projection must be written atomically with the order in the same transaction.

AsyncEventConfig enables @EnableAsync and defines a bounded ThreadPoolTaskExecutor, because relying on Spring's default executor gives an unbounded thread pool and no backpressure. The trade-off of @Async AFTER_COMMIT listeners is that they run outside the transaction: if the email fails, the order still exists, so genuinely critical follow-ups need their own retry or outbox mechanism. This pattern shines for best-effort, non-transactional side effects — notifications, cache invalidation, metrics — where loose coupling matters more than guaranteed delivery.


Related snips

Share this code

Here's the card — post it anywhere.

Transactional Domain Events With Spring's ApplicationEventPublisher and @TransactionalEventListener — share card
Link copied