[EN] `@EventListener` in Spring Boot: So baust du lose gekoppelte Komponenten

Julian | Oct 20, 2025 min read

Hello everyone,

In modern, microservices-oriented architectures, we want to keep components as independent as possible. When service A performs an action (e.g. registers a user), service B should respond to it (e.g. send a welcome email) without A needing to know from B directly.

This is exactly where the Observer pattern shines, implemented by ApplicationEvent and the @EventListener annotation in Spring Boot. This pattern is extremely powerful and allows us to build loosely coupled, highly cohesive components.


The principle: decoupling through events

The core of the Spring Event mechanism is complete decoupling:

  1. The Producer (Source): He carries out an action and publishes an event. He doesn’t know who is listening.
  2. The Consumer (Listener): It subscribes to certain event types and then executes its own logic. He doesn’t know who published the event.
  3. The Broker (ApplicationEventPublisher): Spring Boot takes on the role of the intermediary.

Step 1: Define the event

First we define a simple event object. It should contain all the information the listener needs to complete their task. In modern Java versions (Java 17+), a record is ideal for this.

// src/main/java/com/example/events/UserRegisteredEvent.java

/**
 * Event, das ausgeloest wird, wenn ein neuer Benutzer registriert wurde.
 * Nutzt ein Java Record fuer eine immutable Datenstruktur.
 */
public record UserRegisteredEvent(
    String userId,
    String email,
    long timestamp
) {
    // Statische Factory-Methode (optional, aber guter Stil)
    public static UserRegisteredEvent create(String userId, String email) {
        return new UserRegisteredEvent(userId, email, System.currentTimeMillis());
    }
}

Step 2: Publish the Event (The Producer)

To publish the event, we need the ApplicationEventPublisher, which we simply inject into our service.

// src/main/java/com/example/service/RegistrationService.java

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
public class RegistrationService {

    private final ApplicationEventPublisher eventPublisher;

    // Spring injiziert den Publisher automatisch
    public RegistrationService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void registerNewUser(String username, String email) {
        // 1. Kern-Logik ausfuehren (z.B. Speichern in der DB)
        String newUserId = generateAndSaveUser(username, email);
        
        // 2. Event erstellen
        UserRegisteredEvent event = UserRegisteredEvent.create(newUserId, email);
        
        // 3. Event publizieren – die Registrierung ist erfolgreich abgeschlossen!
        eventPublisher.publishEvent(event);
        
        System.out.println("User " + username + " erfolgreich registriert und Event publiziert.");
    }
    
    private String generateAndSaveUser(String username, String email) {
        // ... (Simulierte Speicherung)
        return "UUID-789-" + username;
    }
}

The RegistrationService only takes care of our job (registration) and has no idea whether an email is sent, a log entry is created or a statistics counter is increased. This is loose coupling at its finest!


Step 3: Consume the Event (The Listener)

The listener is a normal Spring component that simply sets the @EventListener annotation over a method. The method must expect the type of event it is supposed to process as an argument.

a) Synchroner Listener (Standard)

By default, the listener runs synchronously in the same thread where the event was published. Registration blocks until the email is sent (or the listener is finished).

// src/main/java/com/example/listeners/EmailNotificationListener.java

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class EmailNotificationListener {

    // Wird synchron im Thread der RegistrationService ausgefuehrt
    @EventListener
    public void handleRegistration(UserRegisteredEvent event) {
        System.out.println("-> [EMAIL] Sende Willkommens-E-Mail an: " + event.email());
        // ... (Simulierte E-Mail-Versand-Logik)
    }
}

If the listener is performing I/O-intensive tasks (like sending emails, API calls, or database updates), we must run it asynchronously to avoid blocking the application’s main thread.

  1. Let’s enable asynchrony in the main class:
@SpringBootApplication 
@EnableAsync // IMPORTANT: Allows asynchronous execution 
public class DemoApplication { /* ... */ } 
  1. Let’s decorate the listener with @Async:
// src/main/java/com/example/listeners/StatsUpdateListener.java

import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class StatsUpdateListener {

// Runs asynchronously in a separate thread 
@Async 
@EventListener 
public void updateStatistics(UserRegisteredEvent event) { 
System.out.println("-> [STATS] Update statistics for user: " + event.userId()); 
// ... (May take a long time, but does not block registration) 
} 
} 

Additional tricks for @EventListener

1. Conditional Event Handling

We can use Spring Expression Language (SpEL) to run the listener only under certain conditions.

// Fuehrt Listener nur aus, wenn die E-Mail 'julian@example.com' enthaelt
@EventListener(condition = "#event.email.contains('julian@example.com')")
public void handleSpecialUser(UserRegisteredEvent event) {
    System.out.println("!!! Achtung: Special User registriert.");
}

2. Process event hierarchy

If our event inherits from a superclass, we can define a listener that handles all sub-events, or a more specific listener that only catches the exact event type.


Conclusion

The Spring Event mechanism with ApplicationEvent and @EventListener is one of the most underrated patterns in Spring Boot. It is the internal, loosely coupled alternative to messaging via external brokers such as RabbitMQ or Kafka.

We use this pattern whenever we have tasks that need to be performed after a main action, but are not part of the core responsibility of the main component. It is the key to clean, maintainable and flexible code.