In Spring, events let one part of your application publish something that happened, while other parts react to it without being tightly coupled.
Typical use cases:
- Send an email after user registration
- Clear a cache after data changes
- Audit an action
- Trigger async background processing
- React to transaction completion
Spring has built-in support through:
ApplicationEventPublisher@EventListenerApplicationEvent@TransactionalEventListener
1. Define an Event
Modern Spring applications often use a plain Java object as an event. You do not have to extend ApplicationEvent.
public record UserRegisteredEvent(
Long userId,
String email
) {
}
You can also use a normal class:
public class UserRegisteredEvent {
private final Long userId;
private final String email;
public UserRegisteredEvent(Long userId, String email) {
this.userId = userId;
this.email = email;
}
public Long getUserId() {
return userId;
}
public String getEmail() {
return email;
}
}
2. Publish the Event
Inject ApplicationEventPublisher into a Spring-managed bean and call publishEvent.
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final ApplicationEventPublisher eventPublisher;
public UserService(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void registerUser(String email) {
// Save user, validate data, etc.
Long userId = 42L;
eventPublisher.publishEvent(new UserRegisteredEvent(userId, email));
}
}
3. Listen for the Event
Use @EventListener on a method in a Spring bean.
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class UserRegisteredListener {
@EventListener
public void handleUserRegistered(UserRegisteredEvent event) {
System.out.println("User registered: " + event.email());
// Send welcome email, write audit log, etc.
}
}
Spring automatically detects listener methods and invokes them when a matching event is published.
4. Multiple Listeners Can React to the Same Event
You can have several independent listeners for one event.
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class WelcomeEmailListener {
@EventListener
public void sendWelcomeEmail(UserRegisteredEvent event) {
System.out.println("Sending welcome email to " + event.email());
}
}
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class AuditLogListener {
@EventListener
public void audit(UserRegisteredEvent event) {
System.out.println("Audit log for user " + event.userId());
}
}
This keeps the registration logic separate from email, auditing, and other side effects.
5. Listen Only When a Condition Matches
You can add a condition using Spring Expression Language.
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class CorporateUserListener {
@EventListener(condition = "#event.email().endsWith('@company.com')")
public void handleCorporateUser(UserRegisteredEvent event) {
System.out.println("Corporate user registered: " + event.email());
}
}
For a JavaBean-style event class, you might use:
@EventListener(condition = "#event.email.endsWith('@company.com')")
public void handleCorporateUser(UserRegisteredEvent event) {
// ...
}
6. Make Event Handling Asynchronous
By default, Spring event listeners run synchronously in the same thread as the publisher.
To run listeners asynchronously, enable async execution:
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
public class AsyncConfig {
}
Then annotate the listener with @Async.
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class AsyncWelcomeEmailListener {
@Async
@EventListener
public void sendWelcomeEmail(UserRegisteredEvent event) {
System.out.println("Sending email asynchronously to " + event.email());
}
}
You can also configure a custom executor:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
public class AsyncConfig {
@Bean(name = "applicationEventExecutor")
public Executor applicationEventExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("app-event-");
executor.setCorePoolSize(4);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(100);
executor.initialize();
return executor;
}
}
Use it like this:
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class AsyncAuditListener {
@Async("applicationEventExecutor")
@EventListener
public void audit(UserRegisteredEvent event) {
System.out.println("Async audit for user " + event.userId());
}
}
7. Use Transaction-Aware Events
If you publish an event inside a database transaction, a normal @EventListener runs immediately, even before the transaction commits.
If you want the listener to run only after the transaction commits, use @TransactionalEventListener.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.context.ApplicationEventPublisher;
@Service
public class UserService {
private final ApplicationEventPublisher eventPublisher;
public UserService(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
@Transactional
public void registerUser(String email) {
Long userId = 42L;
// Persist user here
eventPublisher.publishEvent(new UserRegisteredEvent(userId, email));
}
}
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;
@Component
public class UserRegisteredTransactionalListener {
@TransactionalEventListener
public void afterCommit(UserRegisteredEvent event) {
System.out.println("Transaction committed for user " + event.userId());
}
}
By default, @TransactionalEventListener runs in the AFTER_COMMIT phase.
You can specify the phase explicitly:
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
@Component
public class UserRegisteredTransactionListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void afterCommit(UserRegisteredEvent event) {
System.out.println("After commit: " + event.email());
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void afterRollback(UserRegisteredEvent event) {
System.out.println("After rollback: " + event.email());
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
public void afterCompletion(UserRegisteredEvent event) {
System.out.println("Transaction completed: " + event.email());
}
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void beforeCommit(UserRegisteredEvent event) {
System.out.println("Before commit: " + event.email());
}
}
8. Listener Ordering
If multiple listeners handle the same event, you can control their order with @Order.
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
public class OrderedListeners {
@Order(1)
@EventListener
public void first(UserRegisteredEvent event) {
System.out.println("First listener");
}
@Order(2)
@EventListener
public void second(UserRegisteredEvent event) {
System.out.println("Second listener");
}
}
Lower order values run first.
9. Returning Events from Listeners
A synchronous listener can return another event, and Spring will publish it.
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class ChainedEventListener {
@EventListener
public AccountCreatedEvent handleUserRegistered(UserRegisteredEvent event) {
return new AccountCreatedEvent(event.userId());
}
}
Example second event:
public record AccountCreatedEvent(Long userId) {
}
Then another listener can react to it:
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class AccountCreatedListener {
@EventListener
public void handleAccountCreated(AccountCreatedEvent event) {
System.out.println("Account created for user " + event.userId());
}
}
Avoid this pattern for complex workflows, though. It can become hard to trace.
10. Legacy ApplicationEvent Style
Older Spring code often defines events by extending ApplicationEvent.
import org.springframework.context.ApplicationEvent;
public class UserRegisteredApplicationEvent extends ApplicationEvent {
private final Long userId;
private final String email;
public UserRegisteredApplicationEvent(Object source, Long userId, String email) {
super(source);
this.userId = userId;
this.email = email;
}
public Long getUserId() {
return userId;
}
public String getEmail() {
return email;
}
}
Publishing:
eventPublisher.publishEvent(
new UserRegisteredApplicationEvent(this, userId, email)
);
Listening:
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class LegacyUserEventListener {
@EventListener
public void handle(UserRegisteredApplicationEvent event) {
System.out.println(event.getEmail());
}
}
This still works, but plain objects or records are usually simpler.
Recommended Pattern
For most Spring applications:
- Use a simple immutable event type, often a
record. - Publish it from a service using
ApplicationEventPublisher. - Listen with
@EventListener. - Use
@TransactionalEventListenerfor database-related side effects. - Use
@Asynconly for work that does not need to complete before the caller continues.
Example:
public record OrderPlacedEvent(
Long orderId,
Long customerId
) {
}
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public OrderService(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
@Transactional
public void placeOrder(Long customerId) {
Long orderId = 100L;
// Save order
eventPublisher.publishEvent(new OrderPlacedEvent(orderId, customerId));
}
}
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;
@Component
public class OrderNotificationListener {
@TransactionalEventListener
public void sendConfirmation(OrderPlacedEvent event) {
System.out.println("Send confirmation for order " + event.orderId());
}
}
This ensures the confirmation runs only after the order transaction successfully commits.
