Simple Guide to Spring Event System

Yousef MeskaYousef Meska
6 min read

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:

  1. Events โ€“ Simple POJO classes that carry data.

  2. Publishers โ€“ Components that create and dispatch events.

  3. 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:

  1. ApplicationListener Interface - Traditional approach with limitations

  2. @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

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

  1. Avoid Infrastructure Interactions within event listeners that are part of a transactional context

  2. Use Transaction-Bound Events for operations that should only occur after successful transactions

  3. Apply Asynchronous Processing for time-consuming operations to prevent blocking

  4. Keep Events Focused - Each event should represent a single, well-defined domain event

  5. 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:

0
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.