Why this matters
- Tight coupling between
OrderServiceand email, analytics, and loyalty-point logic makes every change ripple through the codebase. - Events decouple the publisher from listeners — add a new reaction to
OrderPlacedwithout touching the checkout flow. - Transactional event listeners solve the classic problem of sending notifications before the database commit actually succeeds.
OrderServiceVaultCommerce
vaultcommerce.orders.placed.v1
InventoryConsumer
Event API building blocks
ApplicationEventPublisher— inject this to publish events from any service.- Plain POJO events — any class can be an event; records work well for immutable payloads.
@EventListener— marks a method that reacts to a specific event type.@TransactionalEventListener— defers execution until a transaction commits, rolls back, or completes.@Asyncon listeners — runs the handler off the publishing thread; combine with transactional listeners carefully.
Define and publish an event
public record OrderPlacedEvent(
Long orderId,
String customerEmail,
BigDecimal totalAmount,
Instant placedAt
) {}
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher events;
public OrderService(OrderRepository orderRepository,
ApplicationEventPublisher events) {
this.orderRepository = orderRepository;
this.events = events;
}
@Transactional
public OrderResponse placeOrder(PlaceOrderRequest request) {
Order order = orderRepository.save(buildOrder(request));
events.publishEvent(new OrderPlacedEvent(
order.getId(),
order.getCustomerEmail(),
order.getTotal(),
order.getPlacedAt()
));
return OrderResponse.from(order);
}
}
Listeners in separate beans
@Component
public class OrderEventListeners {
private final LoyaltyPointService loyaltyService;
private final AnalyticsService analytics;
public OrderEventListeners(LoyaltyPointService loyaltyService,
AnalyticsService analytics) {
this.loyaltyService = loyaltyService;
this.analytics = analytics;
}
@EventListener
public void awardLoyaltyPoints(OrderPlacedEvent event) {
loyaltyService.credit(event.customerEmail(), event.totalAmount());
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void trackOrderAnalytics(OrderPlacedEvent event) {
analytics.recordPurchase(event.orderId(), event.totalAmount());
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void logFailedCheckout(OrderPlacedEvent event) {
log.warn("Order {} rolled back after event was published", event.orderId());
}
}
Ordering and conditional listeners
@EventListener
@Order(1)
public void reserveInventory(OrderPlacedEvent event) {
inventoryService.reserve(event.orderId());
}
@EventListener(condition = "#event.totalAmount.compareTo(T(java.math.BigDecimal).valueOf(100)) > 0")
public void flagHighValueOrder(OrderPlacedEvent event) {
fraudService.review(event.orderId());
}
@Configuration
public class EventConfig {
@Bean
public ApplicationListener<ApplicationReadyEvent> warmCatalogCache() {
return event -> catalogService.preloadBestsellers();
}
}
Quick recall
Everything you need if you only revisit this box.
- Inject
ApplicationEventPublisherand callpublishEventwith a plain object or record. @EventListenermethods live in any@Component; Spring wires them at startup.- Default listeners run synchronously on the publisher's thread — keep them fast or mark them
@Async. @TransactionalEventListener(AFTER_COMMIT)is the safe choice for emails, webhooks, and external calls.- Events decouple modules inside one app; they are not a distributed messaging solution.
Test yourself
Answer these before moving on — recall is what makes it stick.