Publishing and Consuming an OrderPlaced Domain Event with Spring's ApplicationEventPublisher
package com.shop.orders.events;
import java.math.BigDecimal;
import java.time.Instant;
public final class OrderPlaced {
private final long orderId;
private final String customerEmail;
private final BigDecimal total;
private final Instant occurredOn;
public OrderPlaced(long orderId, String customerEmail, BigDecimal total) {
this.orderId = orderId;
this.customerEmail = customerEmail;
this.total = total;
this.occurredOn = Instant.now();
}
public long getOrderId() {
return orderId;
}
public String getCustomerEmail() {
return customerEmail;
}
public BigDecimal getTotal() {
return total;
}
public Instant getOccurredOn() {
return occurredOn;
}
}
package com.shop.orders;
import com.shop.orders.events.OrderPlaced;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
private final OrderRepository orders;
private final ApplicationEventPublisher events;
public OrderService(OrderRepository orders, ApplicationEventPublisher events) {
this.orders = orders;
this.events = events;
}
@Transactional
public Order placeOrder(NewOrderCommand command) {
Order order = Order.from(command);
orders.save(order);
// Delivered to listeners only after this transaction commits.
events.publishEvent(new OrderPlaced(
order.getId(),
order.getCustomerEmail(),
order.getTotal()));
return order;
}
}
package com.shop.orders;
import com.shop.orders.events.OrderPlaced;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
@Component
public class OrderEventListeners {
private static final Logger log = LoggerFactory.getLogger(OrderEventListeners.class);
private final MailClient mail;
private final AnalyticsGateway analytics;
public OrderEventListeners(MailClient mail, AnalyticsGateway analytics) {
this.mail = mail;
this.analytics = analytics;
}
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendConfirmation(OrderPlaced event) {
log.info("Sending confirmation for order {}", event.getOrderId());
mail.sendOrderConfirmation(event.getCustomerEmail(), event.getOrderId());
}
@EventListener
public void recordAnalytics(OrderPlaced event) {
analytics.track("order_placed", event.getOrderId(), event.getTotal());
}
}
package com.shop.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "applicationTaskExecutor")
public Executor applicationTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("order-events-");
executor.initialize();
return executor;
}
}
Spring's ApplicationEventPublisher provides an in-process publish/subscribe mechanism that lets a service announce that something meaningful happened without knowing who cares. This snippet models an OrderPlaced domain event and shows how a service publishes it, then how independent listeners react — decoupling the write-path (persisting the order) from the side effects (notifications, analytics) that should follow.
The OrderPlaced event class is a plain immutable value object carrying the orderId, customerEmail, total, and an occurredOn timestamp. Modeling the event as its own type (rather than firing an ApplicationEvent subclass) is the modern idiom: since Spring 4.2 any object can be published as an event, so the payload stays a clean domain concept with no framework coupling. Immutability matters because the same instance is delivered to every listener; a mutable event invites one listener to corrupt state another depends on.
In OrderService, the ApplicationEventPublisher is injected via the constructor and placeOrder first persists the order inside a @Transactional method, then calls events.publishEvent(new OrderPlaced(...)). Publishing after the save — but still within the transaction — is deliberate: it pairs with @TransactionalEventListener on the consumer side so listeners only run once the surrounding transaction actually commits. If the transaction rolls back, the event is discarded and no spurious emails are sent.
OrderEventListeners holds two handlers. The sendConfirmation method is annotated with @TransactionalEventListener(phase = AFTER_COMMIT), guaranteeing the confirmation email fires only for orders that truly persisted. Because it is also marked @Async, the listener runs on a separate thread from a TaskExecutor, so a slow mail server never blocks the request thread that placed the order. The second handler, recordAnalytics, uses a plain @EventListener and returns quickly; it demonstrates that multiple listeners can subscribe to the same event independently and that ordering between them should not be relied upon.
A subtle pitfall this addresses is the interaction between @Async and @TransactionalEventListener: without AFTER_COMMIT, an async listener could observe an entity that is later rolled back, or hit a lazy-loading error because the persistence context is gone. Running after commit sidesteps both. Enabling this requires @EnableAsync (and Spring Boot auto-configures the transactional event infrastructure). The trade-off of in-process events is that delivery is not durable — a crash between commit and listener execution loses the event — so for guaranteed delivery a transactional outbox is preferable. For intra-application decoupling of side effects, though, this pattern keeps the core OrderService focused and the cross-cutting reactions cleanly separated.
Share this code
Here's the card — post it anywhere.