Event-Driven Programming in Spring Boot with @EventListener
Event-driven design lets components react to facts instead of polling or tightly coupling modules. After some action completes, you publish an event; interested listeners consume it and run their own logic. In classic patterns, this aligns with publish–subscribe and the observer pattern.
Core pieces in Spring’s event model:
- Event payload: the data carried from the producer to consumers
- Listener: code that handles a specific event type
- Publisher: the component that fires events into the ApplicationContext
Listening with ApplicationListener
Spring exposes a generic listener interface. Implementing it registers a listener for a specific event type.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
public class BootEventLogger implements ApplicationListener<ApplicationEvent> {
private static final Logger log = LoggerFactory.getLogger(BootEventLogger.class);
@Override
public void onApplicationEvent(ApplicationEvent event) {
log.info("Observed: {}", event.getClass().getSimpleName());
}
}
Bootstrapping a standard Spring Boot application is unchanged:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class EventDemoApp {
public static void main(String[] args) {
SpringApplication.run(EventDemoApp.class, args);
}
}
When the app starts, you’ll see lifecycle events such as ContextRefreshedEvent and ApplicationReadyEvent being logged.
Custom domain event and interface-based listener
Define a custom event. You can publish any object as an event in modern Spring, but extending ApplicationEvent is still common and explicit.
import org.springframework.context.ApplicationEvent;
public class NoticeEvent extends ApplicationEvent {
private final String text;
public NoticeEvent(Object source, String text) {
super(source);
this.text = text;
}
public String getText() {
return text;
}
}
Create a listener that targets this type:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
public class InterfaceBasedNoticeListener implements ApplicationListener<NoticeEvent> {
private static final Logger log = LoggerFactory.getLogger(InterfaceBasedNoticeListener.class);
@Override
public void onApplicationEvent(NoticeEvent event) {
log.info("Interface-based handler got: {}", event.getText());
}
}
Publish the event through ApplicationEventPublisher:
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
@Component
public class DomainEventPublisher {
private final ApplicationEventPublisher publisher;
public DomainEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void publishNotice(String message) {
publisher.publishEvent(new NoticeEvent(this, message));
}
}
Trigger publication via a simple HTTP endpoint:
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/events")
public class EventSampleController {
private final DomainEventPublisher events;
public EventSampleController(DomainEventPublisher events) {
this.events = events;
}
@PostMapping("/notice")
public void raise(@RequestParam String msg) {
events.publishNotice(msg);
}
}
Issuing a POST to /events/notice?msg=hello will trigger both default framework listeners and the NoticeEvent listener.
Annotation-driven listeners with @EventListener
Instead of implementing an interface, you can annotate any bean method with @EventListener. This is method-level and allows several handlers in one class.
Log any ApplicationEvent:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.EventListener;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GlobalEventLogger {
private static final Logger log = LoggerFactory.getLogger(GlobalEventLogger.class);
@EventListener(classes = ApplicationEvent.class)
public void logLifecycle(ApplicationEvent event) {
log.info("Lifecycle observed: {}", event.getClass().getName());
}
}
Handle the custom event with a method listener:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
public class AnnotationBasedNoticeListener {
private static final Logger log = LoggerFactory.getLogger(AnnotationBasedNoticeListener.class);
@EventListener
@Order(0)
public void whenNoticeArrives(NoticeEvent event) {
log.info("Annotation-based handler got: {}", event.getText());
}
}
Interface-based and annotation-based listeners can coexist. If you need a specific call order among listeners, add @Order on the methods or implement Ordered.
How @EventListener works under the hood
When a Spring Boot application creates its context (for a servlet app, an AnnotationConfigServletWebServerApplicationContext), it registers a set of annotation processors. One of them wires up method-level event listeners.
The key registration happens via AnnotatedBeanDefinitionReader, which calls:
public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment env) {
// ...
AnnotationConfigUtils.registerAnnotationConfigProcessors(registry);
}
Among those processors is EventListenerMethodProcessor, a BeanFactory post-processor. After signletons are ready, it scans beans for methods annotated with @EventListener, adapts them to ApplicationListener instances, and registers them with the context:
@Override
public void afterSingletonsInstantiated() {
for (String name : beanFactory.getBeanNamesForType(Object.class)) {
Class<?> type = AutoProxyUtils.determineTargetClass(beanFactory, name);
if (type != null) {
scanAndRegister(name, type);
}
}
}
private void scanAndRegister(String beanName, Class<?> type) {
Map<Method, EventListener> methods = MethodIntrospector.selectMethods(
type, m -> AnnotatedElementUtils.findMergedAnnotation(m, EventListener.class));
for (Method method : methods.keySet()) {
ApplicationListener<?> listener = factory.createApplicationListener(beanName, type, method);
if (listener instanceof ApplicationListenerMethodAdapter adapter) {
adapter.init(applicationContext, evaluator);
}
applicationContext.addApplicationListener(listener);
}
}
The factory variable above represents available EventListenerFactory implementations. Spring provides one for regular @EventListener and another for @TransactionalEventListener.
Transaction-aware listeners
To receive events only in relation to transaction boundaries, use @TransactionalEventListener. For example, process an event after a successful commit:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.transaction.event.TransactionPhase;
@Component
public class TxAwareNoticeListener {
private static final Logger log = LoggerFactory.getLogger(TxAwareNoticeListener.class);
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onCommitted(NoticeEvent event) {
log.info("Delivered after commit: {}", event.getText());
}
}
This handler is invoked only when the event is published within an active transaction and that transaction commits successfully.