Hello! Welcome back to our series on asynchronous and event-driven patterns in Spring.
In our last lesson, we took the first step toward building a decoupled application by learning how to define and publish custom events with ApplicationEventPublisher. We successfully fired a "signal" from our UserService when a new user registered, but so far, that signal has been lost in the ether because nothing was configured to receive it.
Today, we'll build the other half of this mechanism. You will learn how to create listeners to consume application events using the @EventListener annotation. By the end of this lesson, you will be able to complete the full publish-subscribe cycle within a Spring application, creating a more modular and maintainable system.
1. The Publish-Subscribe Pattern in Spring
What we've been building is an implementation of the Publish-Subscribe (or "Pub/Sub") design pattern. This pattern is fundamental to modern, scalable backend systems. It allows components to communicate without knowing about each other's existence, promoting loose coupling.
The core idea is visualized in the diagram below:

The key roles are:
- Publisher: A component that creates and sends an event. In our previous lesson, this was the
UserService. - Event: A simple object that contains data about what happened. We created the
UserRegisteredEvent. - EventMultiCaster: An intermediary (often called an event bus or broker) that receives events from publishers and routes them to the correct listeners. In Spring, this is handled automatically for you.
- Listener (or Subscriber): A component that registers interest in a specific type of event and performs an action when it receives one. This is our focus today.
2. Creating an Event Listener with @EventListener
Before Spring 4.2, creating a listener required implementing the ApplicationListener interface. While that approach still works, the modern and much simpler way is to use the @EventListener annotation.
Here's all you need to do:
- Create a method inside any Spring-managed bean (e.g., a class annotated with
@Component,@Service, etc.). - Annotate the method with
@EventListener. - Define the method to accept a single parameter: the specific event class you want to listen for.
Spring handles the rest. It scans for @EventListener annotations on startup and automatically routes any published event object to the methods whose parameter type matches the event's class.
Let's expand on our previous example. We have a UserService that publishes a UserRegisteredEvent. Now, let's create an EmailNotificationListener to send a welcome email.
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
// 1. Must be a Spring-managed bean
@Component
public class EmailNotificationListener {
// 2. Annotate the method
@EventListener
// 3. The parameter type determines which event is caught
public void handleUserRegistration(UserRegisteredEvent event) {
System.out.println("LISTENER: Received UserRegisteredEvent for email: " + event.getEmail());
System.out.println("LISTENER: Preparing to send welcome email...");
// In a real application, you would call your email sending logic here.
System.out.println("LISTENER: Welcome email logic executed.");
}
}
It's that simple. You don't need to register the listener with the publisher. The components remain completely decoupled.
Spring Application Events and @EventListener Example
The article 'Spring Application Events and @EventListener Example' provides clear, concise code examples for this. Let's review it.
Please read subsection '4.2. Using @EventListener Annotation'. Notice how the method signature handleAddEmployeeEvent(AddEmployeeEvent event) automatically subscribes the listener to events of type AddEmployeeEvent.
To see this implemented in a live coding session, let's watch how the presenter in the Java Techie video creates multiple listeners for the PatientDischargeEvent.
Spring Boot Application Events Explained with Real-Time Examples | @Javatechie
This segment demonstrates creating several listener methods in different services, each annotated with @EventListener to consume the same event.
Watch from 15:17 to 18:18. Pay close attention to how the @EventListener annotation is applied in the BillingServiceHandler, MedicalRecordService, and others. Notice that the only thing connecting the publisher and listener is the PatientDischargeEvent class itself.
3. Synchronous vs. Asynchronous Listeners
A critical concept to understand is that, by default, event listeners are synchronous.
This means when eventPublisher.publishEvent() is called, the publisher's thread of execution stops and waits until every listener has completed its onApplicationEvent or @EventListener method.
Let's see what happens if we run our code with the UserService from the last lesson and the EmailNotificationListener from this one:
// Output from UserService
User registered: user@example.com
Publishing UserRegisteredEvent...
// Execution now jumps to the listener and BLOCKS
LISTENER: Received UserRegisteredEvent for email: user@example.com
LISTENER: Preparing to send welcome email...
LISTENER: Welcome email logic executed.
// Execution now returns to the publisher
User registration method finished.
The publishEvent() call doesn't return until the listener is done. This is useful if the listener's logic is part of the same transaction, but it can be a problem if the listener is slow. Imagine our email service takes 5 seconds to respond; the user would be stuck waiting 5 extra seconds for their registration to complete.
The video you just watched explains this default blocking behavior perfectly.
Spring Boot Application Events Explained with Real-Time Examples | @Javatechie
This section of the video analyzes the application logs to show that the main process waits for all listeners to finish before it completes, demonstrating the synchronous nature of default event handling.
Watch from 18:18 to 21:43. The presenter points out that the 'process completed' log message only appears after all the listener logs, confirming the blocking, synchronous execution.
Making Listeners Asynchronous
The solution is to make the listener run in a separate thread. Since you are already familiar with the @Async annotation, this will be straightforward. Simply add @Async to your @EventListener method.
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class EmailNotificationListener {
@Async // Run this method in a background thread
@EventListener
public void handleUserRegistration(UserRegisteredEvent event) {
// ... same logic ...
}
}
Don't forget to enable asynchronous processing in your application by adding @EnableAsync to a configuration class or your main application class.
With this change, the publishEvent() call will return almost immediately, and the listener's logic will execute on a different thread from the thread pool. The user registration process is no longer blocked by the email-sending logic.
Spring Boot Application Events Explained with Real-Time Examples | @Javatechie
Now, let's see the effect of adding @Async to the listeners in the demo application.
Watch from 21:43 to 25:28. Observe how adding @Async changes the log output. The 'process completed' message now appears before the listeners have finished, and the thread names in the logs (task-1, task-2, etc.) confirm that the listeners are running on separate, concurrent threads.
Test your understanding!
Let's continue with the e-commerce application from the previous lesson. An OrderPlacedEvent is published when an order is created. You need to implement two listeners:
- An
InventoryServicethat synchronously deducts the ordered items from the stock. This must be synchronous to ensure stock levels are correct before the order is fully confirmed to the user. - An
AnalyticsServicethat asynchronously logs the order details for business intelligence purposes. This can be slow and should not block the main order flow.
Provide the Java code for the @Component classes containing these two listeners. You don't need to implement the full logic, just the method signatures and annotations.
Show answer
Here is the solution for the two listener components.
1. InventoryService (Synchronous Listener)
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
@Service
public class InventoryService {
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
System.out.println("SYNC LISTENER (Inventory): Order " + event.getOrderId() + " received.");
System.out.println("Deducting items from stock...");
// Logic to update inventory would go here.
System.out.println("Stock updated.");
}
}
Note: No @Async annotation is used, so this listener runs synchronously by default.
2. AnalyticsService (Asynchronous Listener)
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class AnalyticsService {
@Async
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
System.out.println("ASYNC LISTENER (Analytics): Order " + event.getOrderId() + " received.");
// Simulate a slow operation
try {
Thread.sleep(3000); // 3-second delay
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Logging order details to analytics platform...");
}
}
Note: The @Async annotation ensures this method runs in a background thread, not blocking the publisher.
Conclusion
Fantastic! You have now mastered the complete, end-to-end flow of Spring's application event mechanism. By combining publishers and listeners, you can create highly decoupled, clean, and extensible backend services.
Key Takeaways:
- You can create an event listener by annotating a method in a Spring bean with
@EventListener. - The type of the method's parameter determines which event it will consume.
- By default, listeners operate synchronously, blocking the publisher's thread until they complete.
- You can make a listener asynchronous by adding the
@Asyncannotation, allowing the publisher to continue without waiting.
What's Next?
The event system we've just built is incredibly powerful for communication within a single application. However, in modern microservices architecture, your InventoryService and NotificationService might be completely separate applications running independently. Spring's internal events cannot cross these application boundaries.
To solve this, we need a dedicated, external system. In our next lesson, we will explore the role of message brokers (like RabbitMQ or Kafka), which are designed for reliable, asynchronous communication between distributed services.