Event-driven systems allow multiple components to communicate with each other by sharing any information and, most importantly, without being directly related. The general idea is that certain components generate events or messages and others receive and process them, either synchronously or asynchronously.
>> Read this post in Spanish here <<
The Spring Framework provides a simple event system that is not very well known; a small treasure hidden in plain sight 💎. Even in the bibliography, it is usually mentioned casually and sometimes not even that. It uses the ApplicationContext
(the Spring bean container) as a broker for sending and receiving events among the beans it contains.
Nothing to do with the possibilities offered by powerful (and complex) platforms such as Rabbit MQ or Apache Kafka: communication between different applications or microservices, queue management, support for high workloads… But, in some scenarios, the Spring Framework event system may be enough. It will save us from using oversized solutions for our needs.
In this post, we will take a look at this event system.
Contents
Sample project
I will use use a Maven sample project based on Spring Boot 3.0 (Java 17). The version is irrelevant: we will explore Spring core features that have been available for many years. So don’t worry if you are using older versions of Spring.
Let’s see pom
file. In addition to the basic Spring Boot starter, I have added the spring-boot-starter-test
to write some tests:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.danielme.spring</groupId>
<artifactId>spring-events</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
The class with the main
method that bootstraps Spring:
package com.danielme.spring.events;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class EventsApplication {
public static void main(String[] args) {
SpringApplication.run(EventsApplication.class, args);
}
}
We use the previous class to run the project. It only starts and stops Spring. The code we write will be used in a test class that I will explain in time.
Publishing and listening to events
First steps
The management of an event in Spring requires the creation of three elements: the representation of the event with its associated data, its publishers, and the listeners that observe it.

The event is a Serializable
class that specializes ApplicationEvent
(we will see later that this is not always mandatory). The consequence is that it inherits a timestamp containing the instant of the event creation, as well as source
, an object representing the publisher. This source
object is provided to the constructor of ApplicationEvent
and cannot be null
. Of course, our event class can contain as many fields as we want.
package com.danielme.spring.events.model;
import org.springframework.context.ApplicationEvent;
public class MessageEvent extends ApplicationEvent {
private static final long serialVersionUID = -3762610544324295353L;
private final String message;
public MessageEvent(Object source, String message) {
super(source);
this.message = message;
}
public String getMessage() {
return message;
}
}

The publisher is a Spring bean that receives via injection an instance of ApplicationEventPublisher
(actually, the ApplicationContext
). The publish
method of this bean sends an event of any type.
package com.danielme.spring.events.services;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import com.danielme.spring.events.model.MessageEvent;
@Service
public class DemoPublisherService {
private static final Logger logger = LoggerFactory.getLogger(DemoPublisherService.class);
private final ApplicationEventPublisher publisher;
public DemoPublisherService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void doSomething() {
logger.info("init doSomething method");
MessageEvent messageEvent = new MessageEvent(this, "Hello!!");
publisher.publishEvent(messageEvent);
logger.info("event was published");
}
}

All the beans that implement the ApplicationListener
interface typed for MessageEvent
will receive the event represented by the object messageEvent
.
package com.danielme.spring.events.listeners;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import com.danielme.spring.events.model.MessageEvent;
@Component
public class MessageListener implements ApplicationListener<MessageEvent> {
private static final Logger logger = LoggerFactory.getLogger(MessageListener.class);
public void onApplicationEvent(MessageEvent event) {
logger.info("Event received, content = " + event.getMessage() + ", timestamp " + event.getTimestamp());
}
Better with the @EventListener annotation
That’s it! It was easy. But I see something I don’t like: this interface-based technique is limiting. What do we do if we want to handle more than one type of event in a class? It isn’t an exotic requirement, and yet it is impossible unless we come up with a fix.
Don’t!🛑 Fortunately, since Spring 4.2 (July 2015) it has been possible to mark any public method of a bean with @EventListener
to receive an event. Thanks to this annotation, we can define all the @EventListener
methods we want in a single class.
package com.danielme.spring.events.listeners;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import com.danielme.spring.events.model.MessageEvent;
@Component
public class MessageAnnotatedListener {
private static final Logger logger = LoggerFactory.getLogger(MessageAnnotatedListener.class);
@EventListener
public void listenMessageEvent(MessageEvent event) {
logger.info("Event received, content = {}, timestamp {}", event.getMessage(), event.getTimestamp());
}
}
An additional benefit of getting rid of the ApplicationListener
interface is that we are no longer forced to make the event inherit from ApplicationEvent
. This requirement must only be fulfilled by the events used to type ApplicationListener
.
Now, the parameter of the @EventListener
method indicates the event type to handle. If we don’t want the object with the event, we have to specify its class in the annotation in this way:
@EventListener(MessageEvent.class)
public void listenMessageEvent() {
...
}
A method can receive many events. It is the case of the following example, which also demonstrates three events fired by Spring:
- ContextStartedEvent. An event that happens when an
ApplicationContext
is started (ApplicationContext#start
). - ContextRefreshedEvent. An event that happens when an
ApplicationContext
is started or refreshed (ApplicationContext#refresh
). - ContextStoppedEvent. An event that is triggered when an
ApplicationContext
is stopped (ApplicationContext#stop
).
Spring Boot provides these additional events.
package com.danielme.spring.events.listeners;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.ApplicationContextEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.ContextStartedEvent;
import org.springframework.context.event.ContextStoppedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class ApplicationContextEventsListener {
private static final Logger logger = LoggerFactory.getLogger(ApplicationContextEventsListener.class);
@EventListener({ ContextStartedEvent.class, ContextRefreshedEvent.class, ContextStoppedEvent.class, })
public void listenContextEvents(ApplicationContextEvent event) {
logger.info("context event received: {}", event.getClass().getSimpleName());
}
}
A peculiar feature of @EventListener
is that the method can fire an event by returning its object. In fact, it can return several in a Collection
or an array, and each one will be published as a separate event.
The following UML diagram depicts the relationships between the classes we have written above. Note the absence of any relationship between the publisher and the listeners, other than the fact that they use MessageEvent
.

Testing with JUnit 5 and Mockito
Let’s check that everything works as expected with a JUnit 5 test. It will invoke the DemoPublisherService#doSomething
method to confirm that the listeners are executed.
package com.danielme.spring.events;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import com.danielme.spring.events.listeners.MessageAnnotatedListener;
import com.danielme.spring.events.listeners.MessageListener;
import com.danielme.spring.events.model.MessageEvent;
import com.danielme.spring.events.services.DemoPublisherService;
@SpringBootTest
class EventsTest {
@Autowired
DemoPublisherService publisher;
@SpyBean
MessageAnnotatedListener annotatedListener;
@SpyBean
MessageListener messageListener;
@Test
void testEventsWereListened() {
publisher.doSomething();
verify(annotatedListener, times(1)).listenMessageEvent(any(MessageEvent.class));
verify(messageListener, times(1)).onApplicationEvent(any(MessageEvent.class));
}
}
There are several points to clarify about EventsTest
. First, it is a class with tests that need the Spring context, hence the @SpringBootTest
annotation. By default, those tests are written with Jupiter (JUnit 5).
Any Spring bean can be injected into EventsTest
. The MessageAnnotatedListener
and MessageListener
injections are special because they use @SpyBean
instead of @Autowired
. I want versions of those beans converted to Mockito (*) Spy objects. This allows me to verify if their event-listening methods were executed exactly once (lines 33 and 34).
(*) Library already included in spring-boot-starter-test
.
In the log, we can also check by eye the proper functioning of our event system:
2022-10-01 20:44:19.620 INFO 46136 --- [ main] com.danielme.spring.events.EventsTest : Started EventsTest in 2.03 seconds (JVM running for 3.527) 2022-10-01 20:44:19.670 INFO 46136 --- [ main] c.d.s.e.services.DemoPublisherService : init doSomething method 2022-10-01 20:44:19.685 INFO 46136 --- [ main] c.d.s.events.listeners.MessageListener : Event received, content = Hello!!, timestamp 1664649859670 2022-10-01 20:44:19.690 INFO 46136 --- [ main] c.d.s.e.l.MessageAnnotatedListener : Event received, content = Hello!!, timestamp 1664649859670 2022-10-01 20:44:19.691 INFO 46136 --- [ main] c.d.s.e.services.DemoPublisherService : event was published
It’s easy to spot that the MessageEvent
listeners are executed synchronously in the same thread as the publisher. DemoPublisherService#doSomething
fires the event and waits for all listeners to handle it. It will then continue with the next line of code. This implies that an exception in any of the listeners will propagate to the publisher.

Order
If necessary, you can set the listeners’ execution order by pairing @EventListener
with @Order.
The order is indicated by a number so that the sequence of execution goes from the lowest number (highest priority) to the largest.
The following configuration ensures that MessageAnnotatedListener
will always be executed first. This is the opposite of what the previous log showed:
@EventListener
@Order(1)
public void listenMessageEvent(MessageEvent event) {
Transactions
Regarding transactionality, the listeners will take part in the (hypothetical) ongoing transaction in which the publisher participates, even if they do not have the @Transactional
annotation. Not a surprise: it is the standard behavior.
@TransactionalEventListener
is an @EventListener
that enables you to configure at what point in the current transaction the listener will be executed. Analyzing this annotation is beyond the scope of this post, so I recommend you read this article. Anyway, if you use @TransactionalEventListener
, keep in mind that the listener will only be invoked when the event is fired from a transactional publisher.
Asynchronous listeners with @Async
In practice, it will be usual that the listeners must be executed asynchronously, each one in its own execution thread. In this way, they will not block the execution of the method that publishes the event. It just fires it and continues with its job.

If you read other tutorials, you will see that some authors explain that this can be achieved by creating a bean of type ApplicationEventMulticaster
that Spring will use to call the listeners.
@Bean
ApplicationEventMulticaster applicationEventMulticaster() {
SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();
eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
return eventMulticaster;
}
SimpleApplicationEventMulticaster
is an implementation of ApplicationEventMulticaster
that invokes the listeners in the same thread in which the publisher method runs. That’s what we have done so far: the whole process is synchronous. But if you look at line 4, it is possible to specify an Executor
with which the listeners will be called. It is an interface that executes a given method given as a Runnable
. The selected implementation (SimpleAsyncTaskExecutor
) will execute each call to TaskExecutor#execute
made by ApplicationEventMulticaster
in a new thread.
ApplicationEventMulticaster
will distribute all the events we publish, which means that all the listeners will be executed asynchronously. What if we want some of them to be asynchronous and others not? We will have to find our way and create, for example, our own multicaster.
We have unnecessarily backed ourselves into a corner. We can forget about configuring a multicaster and turn an @EventListener
method into an asynchronous task.
In a nutshell, it is enough to just pair @EventListener
with @Async
. I won’t go into details; check the following post:
Let’s add a new class to the project with an asynchronous listener for MessageEvent
:
package com.danielme.spring.events.listeners;
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 com.danielme.spring.events.model.MessageEvent;
@Component
public class MessageAsyncListener {
private static final Logger logger = LoggerFactory.getLogger(MessageAsyncListener.class);
@EventListener
@Async
public void processMessageEvent(MessageEvent event) {
logger.info("Procesing event asynchronous, content = {} , timestamp {}", event.getMessage(),
event.getTimestamp());
}
}
We see in action one of the great strengths of event-driven systems: adding this new listener for MessageEvent
does not require messing with existing code. The open-closed principle at work.
In order for @Async
to work, don’t forget to annotate with @EnableAsync
a configuration class (one marked with @Configuration
or @SpringBootApplication
):
@SpringBootApplication
@EnableAsync
public class EventsApplication {
Mission accomplished! You can see in the log that when the asynchronous @EventListener
writes its message, doSomething
has already finished:
2022-10-01 22:09:31.050 INFO 49458 --- [ main] c.d.s.e.services.DemoPublisherService : init doSomething method 2022-10-01 22:09:31.063 INFO 49458 --- [ main] c.d.s.events.listeners.MessageListener : Event received, content = Hello!!, timestamp 1664654971050 2022-10-01 22:09:31.067 INFO 49458 --- [ main] c.d.s.e.l.MessageAnnotatedListener : Event received, content = Hello!!, timestamp 1664654971050 2022-10-01 22:09:31.130 INFO 49458 --- [ main] c.d.s.e.services.DemoPublisherService : event was published 2022-10-01 22:09:31.159 INFO 49458 --- [ task-1] c.d.s.e.listeners.MessageAsyncListener : Procesing event asynchronous, content = Hello!! , timestamp 1664654971050
Asynchronous listeners cannot fire an event by returning the event object; they must call the ApplicationEventPublisher
. Also, their exceptions do not affect the publisher, as both run in different threads. What is the same as in synchronous listeners is the use of @Order
and @TransactionalEventListener
.
Testing becomes more complicated as soon as it involves asynchronous code. The next "
improvement"
of the test will fail due to the last verify
because listenAsyncMessageEvent
has not yet been executed:
@Test
void testEventsWereListened() {
publisher.doSomething();
verify(annotatedListener, times(1)).listenMessageEvent(any(MessageEvent.class));
verify(messageListener, times(1)).onApplicationEvent(any(MessageEvent.class));
verify(asyncListener, times(1)).listenAsyncMessageEvent(any(MessageEvent.class));
}
The solution is in the Mockito#timeout
method. It allows setting a timeout in milliseconds for the conditions supported by verify
.
@Test
void testEventsWereListened() {
publisher.doSomething();
verify(annotatedListener, times(1)).listenMessageEvent(any(MessageEvent.class));
verify(messageListener, times(1)).onApplicationEvent(any(MessageEvent.class));
verify(asyncListener, timeout(1000).times(1)).listenAsyncMessageEvent(any(MessageEvent.class));
}
Now the third assertion will be successful if listenAsyncMessageEvent
is executed before one second passes from the instant in which the verification begins to be checked.
Source code
The project is available on GitHub.
Other posts in English
Spring Framework: asynchronous methods with @Async, Future and TaskExecutor
Testing Spring Boot: Docker with Testcontainers and JUnit 5. MySQL and other images.