Simple Guide to Spring Event System


Introduction to Spring Event System
Event-driven architecture is a powerful design pattern that enables loosely coupled communication between components in an application. Spring's Application Event system provides an elegant way to implement this pattern, allowing for clean separation of concerns and improved maintainability.
This article explores the fundamentals of Spring's event system and demonstrates advanced techniques for implementing event-driven patterns in Spring applications.
The Spring Application Event system facilitates communication between components without tight coupling. This approach aligns with several key design principles:
Key Characteristics
Dynamic Listeners: Subscribers (listeners) can be added or removed at runtime without affecting the publisher.
Decoupled Components: Publishers and listeners operate independently, ensuring that changes in one do not impact the other.
Open/Closed Principle: The system allows extending functionality without modifying existing code.
Core Components
The Spring Event system consists of three main components:
Events โ Simple POJO classes that carry data.
Publishers โ Components that create and dispatch events.
Listeners โ Components that react to events.
Basic Implementation
Creating Custom Events
Events in Spring are simple POJOs that encapsulate the data you want to transmit:
public class CustomEvent {
private String name;
// Constructors, getters, setters
}
Publishing Events
To publish events, you need access to the ApplicationEventPublisher
which can be injected into the service you need to publish events from:
@Service
public class EventPublisherService {
@Autowired
private ApplicationEventPublisher publisher;
public void triggerEvent() {
publisher.publishEvent(new CustomEvent("Sample Event"));
}
}
Real-World Example: Customer Registration
Let's examine a common use case: customer registration with follow-up actions like sending welcome emails.
Traditional Approach (Without Events)
@Service
public class CustomerService {
private final CustomerRepository customerRepo;
private final EmailService emailService;
void register(Customer customer) {
customerRepo.save(customer);
emailService.sendEmail(customer);
// Additional operations:
// - Promotion processing
// - External API calls
// - CRM updates
// - etc.
}
void deleteCustomer(Customer customer) {
customerRepo.remove(customer);
emailService.sendRemoveEmail(customer);
}
}
This approach tightly couples the customer registration process with all secondary operations, making the code less maintainable and harder to extend.
Event-Driven Approach
First, create event objects:
@Data
public class CustomerRegistrationEvent {
private final Customer customer;
}
@Data
public class CustomerRemovedEvent {
private final Customer customer;
}
Modify the service to publish events:
@Service
public class CustomerService {
private final CustomerRepository customerRepo;
private final ApplicationEventPublisher publisher;
void register(Customer customer) {
customerRepo.save(customer);
publisher.publishEvent(new CustomerRegistrationEvent(customer));
}
void deleteCustomer(Customer customer) {
customerRepo.remove(customer);
publisher.publishEvent(new CustomerRemovedEvent(customer));
}
}
Create listeners to handle the events:
@Component
@RequiredArgsConstructor
public class EmailListeners {
private final EmailService emailService;
@EventListener
public void onCustomerRegisterEvent(CustomerRegistrationEvent event) {
emailService.sendEmail(event.getCustomer());
}
@EventListener
public void onCustomerDeleteEvent(CustomerRemovedEvent event) {
emailService.sendRemoveEmail(event.getCustomer());
}
}
Event Listener Implementation Approaches
Spring provides two different approaches to implement event listeners:
ApplicationListener Interface - Traditional approach with limitations
@EventListener Annotation - Modern, more flexible approach
ApplicationListener Interface Limitations
Can only be used for objects that extend the ApplicationEvent class
Each listener can only process one event type
@EventListener Annotation
The @EventListener
An annotation provides more flexibility as shown in this comparison:
Key advantages of @EventListener
:
Methods can have non-void return types
Return values are automatically published as new events
Can process multiple event types
The image below demonstrates how event listeners can handle multiple events:
@EventListener
public Foo onEvent(CustomEvent event) {
// Process event
return new Foo(); // Will be sent as an event to the Spring event system
}
If a method returns an array of events, they'll be sent individually to the Spring event system:
Ordering Event Listeners
When multiple listeners handle the same event, you can control their execution order:
Asynchronous Events
By default, Spring events are synchronous, meaning the publisher thread blocks until all listeners complete processing the event.
This can become problematic when listeners perform time-consuming operations:
Implementing Asynchronous Listeners
To make listeners asynchronous, use the @Async
annotation:
Example implementation:
public class AnalyticsService {
@SneakyThrow
public void registerNewCustomer(Customer customer) {
log.info("calling new analytics service: {}", customer);
sleep(5_000); // Simulate long-running operation
}
}
public class AnalyticsCustomerRegisteredListener {
@Async
@EventListener
public void onRegisterEvent(CustomerRegistrationEvent event) {
analyticsService.registerNewCustomer(event.getCustomer());
}
}
Limitations of Asynchronous Listeners
Async listeners cannot publish subsequent events by returning values
Exceptions are not propagated to the caller
- However, you can implement
AsyncUncaughtExceptionHandler
to process any async exceptions
- However, you can implement
Transaction-Bound Events
When working with transactional methods, we need special consideration for events, whenever we want to have a consistent state of a group of calls to be either committed in case of success, or rolledback in case of failure, we wrap the class or the service method with the @Transactional annotation to gurantee this consistent state.
Common Issues with Events in Transactional Methods
But what happens when we've events published from within this method? If any error happens in:
OrderRepository
CustomerRepository
-> which is a step in the operation that will be called from within an event listener,
they both will be rolled back, and we'll not publish the next event (email sending)
But what if an issue happens after all events are published? And we didn't reach commit point yet, so we will rollback, with rolling back we ensured consistent state in the db.
but unfortunately the email is already has been sent!!
A more serious problem occurs when events trigger actions that cannot be rolled back:
@TransactionalEventListener
Spring provides @TransactionalEventListener
to control when events are processed relative to transaction phases:
๐ด It's important to note that, this annotation doesn't make the listener transactional but delays event consumption until a certain transaction outcome occurs.
Available transaction phases for the event to be handled:
BEFORE_COMMIT - Execute before transaction commits
AFTER_COMMIT - Execute after successful transaction commit (default)
AFTER_ROLLBACK - Execute after transaction rollback
AFTER_COMPLETION - Execute after transaction completes (either commit or rollback)
Real-World Example: Order Processing
Consider an order processing scenario where customer rewards should only be granted after a successful transaction:
@Service
public class OrderService {
@Transactional
public void placeOrder(Order order) {
log.info("placing an order {}", order);
order.setStatus(COMPLETED);
orderRepository.save(order);
publisher.publishEvent(new OrderCompletedEvent(order));
}
}
With transaction-bound event listeners, we can ensure rewards are only processed after successful order completion:
@Component
public class RewardListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleOrderCompleted(OrderCompletedEvent event) {
rewardService.processRewards(event.getOrder().getCustomer());
}
}
Best Practices
Avoid Infrastructure Interactions within event listeners that are part of a transactional context
Use Transaction-Bound Events for operations that should only occur after successful transactions
Apply Asynchronous Processing for time-consuming operations to prevent blocking
Keep Events Focused - Each event should represent a single, well-defined domain event
Design for Failure - Consider what happens when listeners fail and implement appropriate error handling
Conclusion
Properly using synchronous, asynchronous, and transaction-bound events allows you to design systems that respond appropriately to application state changes while maintaining data consistency and performance.
References:
LinkedIn Learning - Spring events is a simple course covering the basics of Spring events system
https://blog.codeleak.pl/2017/10/asynchrouns-and-transactional-event.html: shows the different behavior of using various annotations with listeners.
https://www.baeldung.com/spring-events is a more comprehensive writeup about spring events
https://www.baeldung.com/spring-transactional-async-annotation
Subscribe to my newsletter
Read articles from Yousef Meska directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Yousef Meska
Yousef Meska
๐ I'm Yousef, I'm a Software Engineer who is obsessed with Javascript and highly focused on the performance of web applications. My main interest is building scalable backend solutions following best practices and putting security in mind. With +2 experience building resilient products on top of NodeJs and Typescript (PERN, MERN) stacks. You can check yousefmeska.tech for more info. I love writing due to the love of knowing and mastering something, I love digging into the internals of anything I am exposed to, so writing helps me have a clear and strong mental model about that thing. ๐My writings are mainly about: Explaining Backend & Software Engineering Technical concepts. Best Practices of coding Internals of Architecture, tools, and languages we use on a daily basis Software Engineering career guidance and advice.