Spring Boot’s application events let components communicate asynchronously, but they’re not magic — they still rely on a synchronous dispatcher under the hood.

Let’s see it in action. Imagine a simple e-commerce scenario: a user places an order. We want to trigger several independent actions: update inventory, send a confirmation email, and log the order for auditing.

Here’s how we’d set it up:

First, define your event payload. This is just a plain Java object carrying the data relevant to the event.

public class OrderPlacedEvent {
    private final Long orderId;
    private final String customerEmail;
    private final Map<String, Integer> orderItems;

    public OrderPlacedEvent(Long orderId, String customerEmail, Map<String, Integer> orderItems) {
        this.orderId = orderId;
        this.customerEmail = customerEmail;
        this.orderItems = orderItems;
    }

    public Long getOrderId() {
        return orderId;
    }

    public String getCustomerEmail() {
        return customerEmail;
    }

    public Map<String, Integer> getOrderItems() {
        return orderItems;
    }
}

Next, create your event listeners. These are beans that implement the ApplicationListener interface and specify the type of event they’re interested in.

import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class InventoryUpdateListener implements ApplicationListener<OrderPlacedEvent> {

    @Override
    public void onApplicationEvent(OrderPlacedEvent event) {
        System.out.println("Updating inventory for order: " + event.getOrderId());
        // Logic to decrement stock for items in event.getOrderItems()
    }
}
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class EmailNotificationListener implements ApplicationListener<OrderPlacedEvent> {

    @Override
    public void onApplicationEvent(OrderPlacedEvent event) {
        System.out.println("Sending confirmation email to: " + event.getCustomerEmail() + " for order: " + event.getOrderId());
        // Logic to send email
    }
}
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class AuditLogListener implements ApplicationListener<OrderPlacedEvent> {

    @Override
    public void onApplicationEvent(OrderPlacedEvent event) {
        System.out.println("Logging order for audit: " + event.getOrderId());
        // Logic to write to audit log
    }
}

Finally, in your service or controller where the order is processed, you’ll inject ApplicationEventPublisher and publish the event.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
public class OrderService {

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    public void placeOrder(Long orderId, String customerEmail, Map<String, Integer> items) {
        System.out.println("Order placed, publishing event...");
        // ... order processing logic ...

        OrderPlacedEvent orderPlacedEvent = new OrderPlacedEvent(orderId, customerEmail, items);
        eventPublisher.publishEvent(orderPlacedEvent);

        System.out.println("Event published.");
    }
}

When orderService.placeOrder(...) is called, Spring’s ApplicationEventPublisher takes the OrderPlacedEvent and broadcasts it to all registered listeners. The ApplicationEventPublisher is backed by Spring’s ApplicationEventMulticaster, which iterates through the listeners and calls their onApplicationEvent method.

The core problem this solves is decoupling. The OrderService doesn’t need to know about InventoryUpdateListener, EmailNotificationListener, or AuditLogListener. It just needs to know that something will happen when an OrderPlacedEvent is published. This makes your system more modular, testable, and easier to extend. You can add new listeners later without modifying the OrderService at all.

The internal mechanism is surprisingly straightforward. When you call eventPublisher.publishEvent(), Spring looks up all beans that implement ApplicationListener for that specific event type (or a supertype). It then synchronously invokes the onApplicationEvent method on each of these listeners. This means if one listener throws an exception, the others won’t be executed unless you wrap the publishing logic in a try-catch.

The ApplicationEventPublisher interface is a bit of a facade. Under the hood, it delegates to an ApplicationEventMulticaster. The default implementation, SimpleApplicationEventMulticaster, handles the synchronous dispatching. If you need asynchronous event handling, you’d typically configure a different ApplicationEventMulticaster (e.g., one that uses a TaskExecutor).

When you configure an asynchronous ApplicationEventMulticaster using a TaskExecutor, the onApplicationEvent methods will be executed in separate threads managed by that executor. This is crucial for preventing a slow listener from blocking the publisher or other listeners. You’d typically define a TaskExecutor bean (like a ThreadPoolTaskExecutor) and then configure Spring to use it for event multicasting.

The common pitfall is assuming events are inherently asynchronous. While you can make them asynchronous, the default behavior is synchronous. This means a long-running onApplicationEvent method will block the calling thread until it completes. If you’re publishing many events or have listeners that perform I/O, you must consider asynchronous processing to avoid performance bottlenecks.

The next logical step after mastering basic application events is exploring how to make them truly asynchronous and handle potential failures in those asynchronous listeners.

Want structured learning?

Take the full Spring-boot course →