Zum Hauptinhalt springen

Woche 8, Tag 2 — Spring Events

Ziel

Heute verstehst du Spring Application Events.

Die Kernfragen:

  1. Was ist ein Spring-Event?
  2. Warum Events nutzen?
  3. Was ist ApplicationEventPublisher?
  4. Was ist @EventListener?
  5. Sind Spring-Events synchron oder asynchron?
  6. Was ist ein transaktionales Event?
  7. Was ist @TransactionalEventListener?
  8. Was bedeutet AFTER_COMMIT?
  9. Wann solltest du Events nutzen?
  10. Wann solltest du keine Events nutzen?
  11. Was sind häufige Prüfungsfallen?

1. Kurz-Wiederholung aus Woche 8, Tag 1

In Tag 1 hast du gelernt:

  • AOP bedeutet Aspect-Oriented Programming.
  • AOP trennt Querschnittsbelange von der Geschäftslogik.
  • Spring AOP ist proxy-basiert.
  • AOP funktioniert, wenn Aufrufe über den Spring-Proxy laufen.
  • Selbstaufruf umgeht den Proxy.
  • @Transactional, Methodensicherheit, Caching und Async setzen oft auf Proxies.
  • Nutze AOP für technische Querschnittsbelange.

Merksatz:


Spring AOP funktioniert, indem Methoden über einen Proxy aufgerufen werden.

Heute lernst du einen weiteren Spring-Kommunikationsmechanismus:


Spring Application Events.

2. Was ist ein Spring-Event?

Ein Spring-Event ist eine Nachricht, die innerhalb des Spring Application Context veröffentlicht wird.

Einfache Idee:


Etwas ist passiert.
Andere Teile der Anwendung können darauf reagieren.

Beispiele:


UserRegisteredEvent
TaskCreatedEvent
InvoicePaidEvent
DocumentUploadedEvent
PasswordResetRequestedEvent
ClientOnboardedEvent

Statt dass ein Service viele andere Services direkt aufruft, kann er ein Event veröffentlichen.

Merksatz:

Ein Spring-Event sagt: Etwas ist in der Anwendung passiert.


3. Warum Events nutzen?

Ohne Events:


public void registerUser(RegisterUserRequest request) {
User user = userRepository.save(...);

emailService.sendWelcomeEmail(user);
auditService.auditUserRegistered(user);
crmService.syncUser(user);
notificationService.notifyAdmin(user);
}

Problem:


UserService kennt zu viele andere Services.
Die Methode wird lang.
Eine neue Reaktion erfordert Änderungen am UserService.
Geschäftsaktion und Nebeneffekte sind vermischt.

Mit Events:


public void registerUser(RegisterUserRequest request) {
User user = userRepository.save(...);

eventPublisher.publishEvent(new UserRegisteredEvent(user.getId(), user.getEmail()));
}

Dann reagieren Listener:


WelcomeEmailListener
AuditListener
CrmSyncListener
AdminNotificationListener

Merksatz:

Events reduzieren die direkte Kopplung zwischen der Hauptaktion und Nebeneffekten.


4. Direkter Aufruf vs. Event

Direkter Aufruf


UserService -> EmailService
UserService -> AuditService
UserService -> NotificationService

Event-Stil


UserService -> veröffentlicht UserRegisteredEvent

WelcomeEmailListener hört zu
AuditListener hört zu
NotificationListener hört zu

Wichtig:


Der Publisher muss nicht alle Listener kennen.

Merksatz:

Der Publisher kennt das Event, nicht jede Reaktion.


5. Event-Publisher

Spring stellt bereit:


ApplicationEventPublisher

Damit veröffentlichst du Events.

Beispiel:


@Service
public class UserRegistrationService {

private final UserRepository userRepository;
private final ApplicationEventPublisher eventPublisher;

public UserRegistrationService(
UserRepository userRepository,
ApplicationEventPublisher eventPublisher
) {
this.userRepository = userRepository;
this.eventPublisher = eventPublisher;
}

@Transactional
public UserDto register(RegisterUserRequest request) {
UserEntity user = new UserEntity(request.email());

UserEntity saved = userRepository.save(user);

eventPublisher.publishEvent(
new UserRegisteredEvent(
saved.getId(),
saved.getEmail()
)
);

return new UserDto(saved.getId(), saved.getEmail());
}
}

Merksatz:

ApplicationEventPublisher veröffentlicht Events in den Spring Application Context.


6. Event-Klasse

Moderne Spring-Events können einfache POJOs oder Records sein.

Beispiel:


public record UserRegisteredEvent(
Long userId,
String email
) {
}

Der ältere Stil kann erweitern:


ApplicationEvent

Beispiel:


public class UserRegisteredEvent extends ApplicationEvent {

private final Long userId;
private final String email;

public UserRegisteredEvent(Object source, Long userId, String email) {
super(source);
this.userId = userId;
this.email = email;
}

public Long getUserId() {
return userId;
}

public String getEmail() {
return email;
}
}

Für den meisten modernen App-Code sind einfache Records sauber.

Merksatz:

Event-Klassen sollten beschreiben, was passiert ist.


7. Event-Listener

Ein Listener reagiert auf Events.

Beispiel:


@Component
public class WelcomeEmailListener {

private final EmailService emailService;

public WelcomeEmailListener(EmailService emailService) {
this.emailService = emailService;
}

@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}
}

Wenn UserRegisteredEvent veröffentlicht wird, ruft Spring diese Methode auf.

Merksatz:

@EventListener markiert eine Methode, die auf ein Event reagiert.


8. Mehrere Listener

Ein Event kann viele Listener haben.


@Component
public class AuditListener {

@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
System.out.println("Audit user registered: " + event.userId());
}
}

@Component
public class AdminNotificationListener {

@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
System.out.println("Notify admin about user: " + event.email());
}
}

Eine Veröffentlichung:


eventPublisher.publishEvent(new UserRegisteredEvent(1L, "user@example.com"));

Kann auslösen:


WelcomeEmailListener
AuditListener
AdminNotificationListener

Merksatz:

Ein Event kann viele Listener haben.


9. Event-Ablauf


Service-Methode

eventPublisher.publishEvent(event)

Spring ApplicationEventMulticaster

passende @EventListener-Methoden

Listener-Logik läuft

Einfaches Bild:


UserRegistrationService
veröffentlicht UserRegisteredEvent

WelcomeEmailListener
AuditListener
AdminNotificationListener

Merksatz:

Spring verteilt veröffentlichte Events an passende Listener.


10. Sind Spring-Events synchron?

Standardmäßig sind Spring-Event-Listener synchron.

Das bedeutet:


publishEvent() wartet, bis die Listener fertig sind.

Beispiel:


eventPublisher.publishEvent(new UserRegisteredEvent(...));

Wenn der Listener eine E-Mail sendet und 3 Sekunden braucht:


publishEvent() kann ebenfalls 3 Sekunden dauern.

Wichtig:


Standard-Spring-Events sind nicht automatisch asynchron.

Merksatz:

Standardmäßig sind Spring-Events synchron.


11. Folge synchroner Events

Wenn der Listener eine Exception wirft:


@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
throw new RuntimeException("Email failed");
}

Dann kann auch der Publisher fehlschlagen, weil der Listener im selben Thread läuft.

Wenn die Publisher-Methode transaktional ist, kann das die Transaktion beeinflussen.

Merksatz:

Bei synchronen Events kann ein Listener-Fehler den Publisher beeinflussen.


12. Soll ein Listener-Fehler den Haupt-Use-Case abbrechen?

Stelle dir diese Design-Frage:


Wenn die Willkommens-E-Mail fehlschlägt, soll die Benutzerregistrierung fehlschlagen?

Manchmal ja:


Zahlungsvalidierung fehlgeschlagen
Betrugsprüfung fehlgeschlagen
Verpflichtende Domänenregel verletzt

Oft nein:


Willkommens-E-Mail fehlgeschlagen
Audit-Logging vorübergehend fehlgeschlagen
Benachrichtigung fehlgeschlagen
Analytics-Event fehlgeschlagen

Wenn ein Fehler den Haupt-Use-Case nicht abbrechen soll, behandle ihn sorgfältig.

Merksatz:

Entscheide, ob ein Listener-Fehler die Hauptaktion abbrechen soll.


13. Transaktionsproblem

Stell dir diesen Service vor:


@Transactional
public UserDto register(RegisterUserRequest request) {
UserEntity user = userRepository.save(new UserEntity(request.email()));

eventPublisher.publishEvent(
new UserRegisteredEvent(user.getId(), user.getEmail())
);

return new UserDto(user.getId(), user.getEmail());
}

Standard-Event-Listener:


@EventListener
public void sendWelcomeEmail(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}

Problem:


Der Event-Listener kann laufen, bevor die Transaktion committed wird.

Wenn die Transaktion später zurückgerollt wird:


Die E-Mail wurde möglicherweise schon gesendet, obwohl der Benutzer nicht committed wurde.

Merksatz:

Ein normaler @EventListener kann laufen, bevor die Transaktion committed wird.


14. Transaktionaler Event-Listener

Nutze:


@TransactionalEventListener

wenn der Listener abhängig vom Transaktionsergebnis laufen soll.

Beispiel:


@Component
public class WelcomeEmailListener {

private final EmailService emailService;

public WelcomeEmailListener(EmailService emailService) {
this.emailService = emailService;
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onUserRegistered(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}
}

Das bedeutet:


Führe diesen Listener aus, nachdem die Transaktion erfolgreich committed wurde.

Merksatz:

@TransactionalEventListener(AFTER_COMMIT) läuft nur nach erfolgreichem Commit.


15. Transaktionsphasen

Gängige Transaktionsphasen:


BEFORE_COMMIT
AFTER_COMMIT
AFTER_ROLLBACK
AFTER_COMPLETION

Bedeutung:

PhaseBedeutung
BEFORE_COMMITvor dem Commit der Transaktion
AFTER_COMMITnach erfolgreichem Commit
AFTER_ROLLBACKnach Rollback
AFTER_COMPLETIONnach Commit oder Rollback

Am häufigsten für Nebeneffekte:


@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)

Merksatz:

Nutze AFTER_COMMIT für Nebeneffekte, die erst nach erfolgreichem Datenbank-Commit passieren sollen.


16. @EventListener vs. @TransactionalEventListener

Thema@EventListener@TransactionalEventListener
Transaktionsbewusstnein, normaler Event-Listenerja
Standard-Timingsofort bei Veröffentlichungan Transaktionsphase gebunden
Typischer Einsatzeinfache In-Memory-ReaktionAfter-Commit-Nebeneffekte
Läuft ohne Transaktionjastandardmäßig nein
Beispieleinfaches Event loggenE-Mail senden nach Benutzer-Save

Merksatz:

Nutze normale Events für einfache Reaktionen; transaktionale Events für transaktionssensitive Nebeneffekte.


17. Fallback-Ausführung

@TransactionalEventListener erwartet normalerweise eine Transaktion.

Wenn keine Transaktion existiert, läuft er möglicherweise nicht.

Es gibt eine Option:


@TransactionalEventListener(
phase = TransactionPhase.AFTER_COMMIT,
fallbackExecution = true
)

Bedeutung:


Wenn keine Transaktion existiert, trotzdem ausführen.

Nutze das vorsichtig.

Merksatz:

fallbackExecution = true lässt den transaktionalen Listener auch ohne Transaktion laufen.


18. Asynchrone Events

Manchmal soll Listener-Arbeit asynchron passieren.

Beispiel:


E-Mail senden
CRM synchronisieren
Analytics-Event senden
PDF generieren
externes System benachrichtigen

Nutze @Async am Listener:


@Component
public class WelcomeEmailListener {

private final EmailService emailService;

public WelcomeEmailListener(EmailService emailService) {
this.emailService = emailService;
}

@Async
@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}
}

Async aktivieren:


@Configuration
@EnableAsync
public class AsyncConfig {
}

Merksatz:

@Async kann Event-Listener-Arbeit in einem anderen Thread ausführen lassen.


19. Async-Event-Falle

@Async nutzt ebenfalls Spring-Proxy/Interception.

Wichtig:


Der Listener-Bean muss ein Spring Bean sein.
@EnableAsync muss aktiviert sein.
Async-Ausführung nutzt einen anderen Thread.
ThreadLocal-Kontext ist nicht automatisch verfügbar.
Exceptions werden anders behandelt.

Beispielproblem:


SecurityContext ist möglicherweise nicht automatisch verfügbar.
Transaktionskontext ist nicht derselbe Thread.
MDC/Logging-Kontext wird möglicherweise nicht automatisch übernommen.

Merksatz:

Asynchrone Listener laufen nicht im selben Thread-Kontext.


20. Async + transaktionales Event

Gängiges Muster:


@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onUserRegistered(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}

Bedeutung:


Nach Transaktions-Commit das Event asynchron verarbeiten.

Gut für:


E-Mails
Benachrichtigungen
externe Synchronisation
Analytics

Aber denk daran:


Das ist immer noch In-Process-Async.
Wenn die App abstürzt, kann das Event verloren gehen.

Merksatz:

Asynchrone transaktionale Events sind nützlich, aber kein durable Messaging.


21. Spring-Events sind In-Process

Spring Application Events sind In-Process.

Das bedeutet:


innerhalb einer laufenden Anwendungsinstanz
innerhalb einer JVM
nicht automatisch gespeichert
nicht automatisch wiederholt
nicht automatisch mit anderen Services geteilt

Wenn die App nach dem Commit, aber vor dem Ende des Async-Listeners abstürzt:


Event kann verloren gehen

Für durable Messaging nutze:


Kafka
RabbitMQ
SQS
Database-Outbox-Pattern
Message Broker

Merksatz:

Spring-Events sind kein Ersatz für durable Messaging.


22. Gutes Event-Design

Gute Event-Namen:


UserRegisteredEvent
TaskCompletedEvent
InvoicePaidEvent
DocumentUploadedEvent
ClientCreatedEvent
PasswordResetRequestedEvent

Schlechte Event-Namen:


SendEmailEvent
CallAuditServiceEvent
DoNotificationEvent

Warum?


Gutes Event sagt, was passiert ist.
Schlechtes Event sagt, was der Listener tun soll.

Merksatz:

Benenne Events nach Fakten, nicht nach Befehlen.


23. Was soll ein Event enthalten?

Gute Event-Payload:


public record UserRegisteredEvent(
Long userId,
String email
) {
}

Üblicherweise enthalten:


IDs
kleine unveränderliche Daten
Geschäftsfakten
Timestamp, falls nützlich
Tenant-ID, falls nötig

Vermeide:


große Entity-Graphen
verwaltete JPA-Entities
sensible Daten
Klartext-Passwörter
zu viel veränderlicher Zustand

Merksatz:

Events sollten kleine, unveränderliche Fakten sein.


24. JPA-Entities in Events vermeiden

Schlecht:


public record UserRegisteredEvent(UserEntity user) {
}

Probleme:


Entity kann später lazy geladen werden
Entity kann detached sein
Listener kann Entity versehentlich ändern
Payload wird groß
Transaktionsgrenze wird verwirrend

Besser:


public record UserRegisteredEvent(
Long userId,
String email
) {
}

Merksatz:

Bevorzuge IDs und einfache Werte statt JPA-Entities in Events.


25. Event-Beispiel: Task erstellt

Event:


public record TaskCreatedEvent(
Long taskId,
Long tenantId,
String title
) {
}

Publisher:


@Service
public class TaskService {

private final TaskRepository taskRepository;
private final ApplicationEventPublisher eventPublisher;

public TaskService(
TaskRepository taskRepository,
ApplicationEventPublisher eventPublisher
) {
this.taskRepository = taskRepository;
this.eventPublisher = eventPublisher;
}

@Transactional
public TaskDto create(CreateTaskRequest request, CurrentUser currentUser) {
TaskEntity task = new TaskEntity(
request.title(),
currentUser.tenantId()
);

TaskEntity saved = taskRepository.save(task);

eventPublisher.publishEvent(
new TaskCreatedEvent(
saved.getId(),
saved.getTenantId(),
saved.getTitle()
)
);

return new TaskDto(saved.getId(), saved.getTitle());
}
}

Listener:


@Component
public class TaskCreatedAuditListener {

private final AuditService auditService;

public TaskCreatedAuditListener(AuditService auditService) {
this.auditService = auditService;
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void audit(TaskCreatedEvent event) {
auditService.recordTaskCreated(
event.taskId(),
event.tenantId(),
event.title()
);
}
}

Merksatz:

Veröffentliche Domänenfakten aus Services und reagiere in Listenern.


26. Event-Beispiel: Passwort-Reset

Event:


public record PasswordResetRequestedEvent(
Long userId,
String email,
String resetToken
) {
}

Publisher:


@Transactional
public void requestPasswordReset(String email) {
UserEntity user = userRepository.findByEmail(email)
.orElseThrow();

String token = resetTokenService.createToken(user);

eventPublisher.publishEvent(
new PasswordResetRequestedEvent(
user.getId(),
user.getEmail(),
token
)
);
}

Listener:


@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendResetEmail(PasswordResetRequestedEvent event) {
emailService.sendPasswordResetEmail(
event.email(),
event.resetToken()
);
}

Wichtig:


Sei vorsichtig mit sensiblen Tokens.
Logge sie nicht.

Merksatz:

Events können E-Mails nach dem Commit auslösen, aber sensible Daten müssen geschützt werden.


27. Listener-Reihenfolge

Wenn mehrere Listener existieren, kann die Reihenfolge wichtig sein.

Nutze:


@Order(1)

Beispiel:


@Component
public class FirstListener {

@Order(1)
@EventListener
public void handle(UserRegisteredEvent event) {
}
}

@Component
public class SecondListener {

@Order(2)
@EventListener
public void handle(UserRegisteredEvent event) {
}
}

Aber verlasse dich nicht zu stark auf die Listener-Reihenfolge.

Wenn die Reihenfolge geschäftskritisch ist, kann direkte Orchestrierung klarer sein.

Merksatz:

Listener-Reihenfolge existiert, aber geschäftskritische Reihenfolge sollte explizit sein.


28. Events aus Listenern zurückgeben

Eine @EventListener-Methode kann einen Rückgabewert haben, der ein weiteres Event wird.

Beispiel:


@EventListener
public WelcomeEmailEvent onUserRegistered(UserRegisteredEvent event) {
return new WelcomeEmailEvent(event.userId(), event.email());
}

Das kann nützlich sein, macht den Ablauf aber auch schwerer verständlich.

Für die Prüfung merk dir:


Event-Listener können weitere Events auslösen.

Merksatz:

Rückgabewerte von Listenern können Folge-Events veröffentlichen — nutze das vorsichtig.


29. Bedingter Event-Listener

@EventListener kann Bedingungen nutzen.

Beispiel:


@EventListener(condition = "#event.email.endsWith('@example.com')")
public void handleExampleDomainUser(UserRegisteredEvent event) {
// nur example.com-Benutzer
}

Das nutzt SpEL.

Manchmal nützlich, aber nicht übermäßig nutzen.

Merksatz:

Event-Listener können bedingt sein.


30. Events vs. Methodenaufrufe

Nutze direkte Methodenaufrufe, wenn:


die Aktion sofort erforderlich ist
der Aufrufer das Ergebnis braucht
die Reihenfolge wichtig ist
die Logik Teil desselben Use-Cases ist
ein Fehler die Hauptoperation abbrechen soll

Nutze Events, wenn:


etwas passiert ist
andere Komponenten reagieren können
der Publisher nicht alle Reaktionen kennen soll
Nebeneffekte getrennt werden können
später neue Listener hinzukommen können

Merksatz:

Direkter Aufruf für erforderliche Arbeit; Event für Reaktionen auf etwas, das passiert ist.


31. Events vs. Message Broker

Nutze Spring-Events für:


In-Process-Entkopplung
dieselbe Anwendung
einfache Nebeneffekte
lokale Modul-Kommunikation
After-Commit-Listener

Nutze einen Message Broker für:


Kommunikation zwischen Services
durable Zustellung
Retry
Backpressure
Event-Replay
verteilte Systeme
Event-Driven Architecture über Apps hinweg

Merksatz:

Spring-Events sind lokal; Broker sind verteilt und durable.


32. Events und Hexagonal/Clean Architecture

Events können Clean Architecture unterstützen.

Application Service:


führt den Haupt-Use-Case aus
veröffentlicht ein Domänen-Event

Listener/Adapter:


E-Mail senden
Audit-Log schreiben
externes System benachrichtigen
Suchindex synchronisieren

Das hält den Kern-Use-Case sauberer.

Aber nutze Events nicht, um unklaren Kontrollfluss zu verstecken.

Merksatz:

Events können Module entkoppeln, aber zu viele Events können den Ablauf verstecken.


33. Event-Veröffentlichung testen

Service-Test mit gemocktem Publisher:


class UserRegistrationServiceTest {

private final UserRepository userRepository = mock(UserRepository.class);
private final ApplicationEventPublisher eventPublisher =
mock(ApplicationEventPublisher.class);

private final UserRegistrationService service =
new UserRegistrationService(userRepository, eventPublisher);

@Test
void publishesUserRegisteredEvent() {
when(userRepository.save(any(UserEntity.class)))
.thenReturn(new UserEntity(1L, "user@example.com"));

service.register(new RegisterUserRequest("user@example.com"));

ArgumentCaptor<UserRegisteredEvent> captor =
ArgumentCaptor.forClass(UserRegisteredEvent.class);

verify(eventPublisher).publishEvent(captor.capture());

UserRegisteredEvent event = captor.getValue();

assertThat(event.userId()).isEqualTo(1L);
assertThat(event.email()).isEqualTo("user@example.com");
}
}

Merksatz:

Unit-Tests können prüfen, dass ein Service das richtige Event veröffentlicht.


34. Event-Listener testen

Listener-Unit-Test:


class WelcomeEmailListenerTest {

private final EmailService emailService = mock(EmailService.class);

private final WelcomeEmailListener listener =
new WelcomeEmailListener(emailService);

@Test
void sendsWelcomeEmail() {
listener.onUserRegistered(
new UserRegisteredEvent(1L, "user@example.com")
);

verify(emailService).sendWelcomeEmail("user@example.com");
}
}

Das testet nicht die Spring-Event-Verteilung.

Es testet die Listener-Logik.

Merksatz:

Listener-Logik kann als normaler Unit-Test getestet werden.


35. Spring-Event-Integration testen

Integrationstest:


@SpringBootTest
class EventIntegrationTest {

@Autowired
private ApplicationEventPublisher eventPublisher;

@MockitoBean
private EmailService emailService;

@Test
void listenerReceivesPublishedEvent() {
eventPublisher.publishEvent(
new UserRegisteredEvent(1L, "user@example.com")
);

verify(emailService).sendWelcomeEmail("user@example.com");
}
}

Das beweist:


Spring verteilt das Event an den Listener
Listener ruft den E-Mail-Service auf

Merksatz:

Nutze den Spring-Test-Context, um echte Event-Verteilung zu testen.


36. Transaktionalen Event-Listener testen

Transaktionale Event-Listener sind schwerer zu testen, weil sie von Commit/Rollback abhängen.

Beispielansatz:


@SpringBootTest
class TransactionalEventIntegrationTest {

@Autowired
private UserRegistrationService userRegistrationService;

@MockitoBean
private EmailService emailService;

@Test
void sendsEmailAfterCommit() {
userRegistrationService.register(
new RegisterUserRequest("user@example.com")
);

verify(emailService).sendWelcomeEmail("user@example.com");
}
}

Aber sei vorsichtig:


Wenn die Testmethode selbst @Transactional ist und zurückrollt,
läuft der AFTER_COMMIT-Listener möglicherweise nicht.

Merksatz:

AFTER_COMMIT-Listener brauchen einen echten Commit, um zu laufen.


37. Häufige Test-Falle

Schlechter Test:


@SpringBootTest
@Transactional
class TransactionalEventTest {

@Test
void sendsEmailAfterCommit() {
userRegistrationService.register(...);

verify(emailService).sendWelcomeEmail(...);
}
}

Problem:


Die Test-Transaktion rollt nach dem Test zurück.
Der AFTER_COMMIT-Listener läuft möglicherweise nicht.

Lösungsoptionen:


@Transactional nicht auf den Test setzen
TestTransaction nutzen, um die Transaktion manuell zu beenden/committen
Listener-Logik separat testen
Integrationstest-Design sorgfältig planen

Merksatz:

Erwarte nicht, dass ein AFTER_COMMIT-Event innerhalb einer zurückgerollten Test-Transaktion läuft.


38. Häufige Prüfungsfallen

Falle 1

Spring-Events sind In-Process-Events.


Falle 2

Standardmäßig sind Spring-Event-Listener synchron.


Falle 3

publishEvent() kann warten, bis die Listener fertig sind.


Falle 4

Listener-Exceptions können den Publisher im synchronen Modus beeinflussen.


Falle 5

Ein normaler @EventListener ist nicht transaktionsphasenbewusst.


Falle 6

@TransactionalEventListener(AFTER_COMMIT) läuft nach erfolgreichem Commit.


Falle 7

AFTER_COMMIT-Listener laufen möglicherweise nicht, wenn die Transaktion zurückrollt.


Falle 8

@TransactionalEventListener läuft möglicherweise ohne Transaktion nicht, außer mit fallbackExecution = true.


Falle 9

@Async braucht @EnableAsync.


Falle 10

Asynchrone Listener laufen in einem anderen Thread.


Falle 11

Spring-Events sind kein durable Messaging.


Falle 12

Übergib keine großen JPA-Entities in Events.


Falle 13

Benenne Events nach dem, was passiert ist, nicht nach dem, was getan werden soll.


Falle 14

Nutze direkte Methodenaufrufe, wenn der Aufrufer das Ergebnis braucht oder ein Fehler den Use-Case stoppen soll.


Falle 15

Nutze Message Broker, wenn durable Zustellung zwischen Services nötig ist.


39. Echte Prüfungsfrage: Spring-Event

Frage:

Was ist ein Spring Application Event?

Antwort:

Ein Spring Application Event ist ein Objekt, das innerhalb des Application Context veröffentlicht wird, um zu signalisieren, dass etwas passiert ist. Spring verteilt das Event an passende Listener.


40. Echte Prüfungsfrage: ApplicationEventPublisher

Frage:

Wofür wird ApplicationEventPublisher genutzt?

Antwort:

Damit veröffentlichst du Events in den Spring Application Context, indem du publishEvent(...) aufrufst.


41. Echte Prüfungsfrage: @EventListener

Frage:

Was macht @EventListener?

Antwort:

Es markiert eine Spring-Bean-Methode als Event-Listener. Spring ruft die Methode auf, wenn ein passendes Event veröffentlicht wird.


42. Echte Prüfungsfrage: synchron oder asynchron

Frage:

Sind Spring-Events standardmäßig asynchron?

Antwort:

Nein. Standardmäßig sind Spring-Event-Listener synchron.


43. Echte Prüfungsfrage: transaktionales Event

Frage:

Wofür wird @TransactionalEventListener genutzt?

Antwort:

Damit bindest du die Listener-Ausführung an eine Transaktionsphase, zum Beispiel nach Commit oder nach Rollback.


44. Echte Prüfungsfrage: AFTER_COMMIT

Frage:

Was bedeutet TransactionPhase.AFTER_COMMIT?

Antwort:

Der Listener läuft nur, nachdem die umgebende Transaktion erfolgreich committed wurde.


45. Echte Prüfungsfrage: asynchroner Listener

Frage:

Was brauchst du, um @Async auf Event-Listenern zu nutzen?

Antwort:

Die Listener-Methode muss auf einem Spring Bean liegen, und Async-Support muss mit @EnableAsync aktiviert sein.


46. Interview-Antwort

Frage:

Erkläre Spring-Events in einfachen Worten.

Gute Antwort:

Spring-Events erlauben einem Teil der Anwendung zu veröffentlichen, dass etwas passiert ist, und andere Spring Beans können zuhören und reagieren. Zum Beispiel kann der Service nach einer Benutzerregistrierung UserRegisteredEvent veröffentlichen, und separate Listener können eine Willkommens-E-Mail senden, ein Audit-Log schreiben oder einen Admin benachrichtigen. Das reduziert die direkte Kopplung zwischen dem Haupt-Service und Nebeneffekten.


47. Interview-Antwort

Frage:

Sind Spring-Events synchron oder asynchron?

Gute Antwort:

Standardmäßig sind Spring-Events synchron. Der publishEvent()-Aufruf wartet, bis die Listener fertig sind. Wenn ich asynchrone Verarbeitung will, kann ich @Async mit @EnableAsync nutzen oder den Event-Multicaster konfigurieren — aber ich muss bedenken, dass asynchrone Listener in einem anderen Thread laufen und kein durable Messaging sind.


48. Interview-Antwort

Frage:

Warum @TransactionalEventListener(AFTER_COMMIT) nutzen?

Gute Antwort:

Ich nutze AFTER_COMMIT, wenn ein Nebeneffekt erst nach erfolgreichem Commit der Datenbank-Transaktion passieren soll. Zum Beispiel sollte das Senden einer Willkommens-E-Mail nach der Benutzerregistrierung üblicherweise nur passieren, wenn der Benutzer wirklich gespeichert wurde. Ein normaler @EventListener kann laufen, bevor die Transaktion committed wird — das kann Nebeneffekte für zurückgerollte Daten verursachen.


49. Interview-Antwort

Frage:

Was ist der Unterschied zwischen Spring-Events und Kafka?

Gute Antwort:

Spring-Events sind lokale, In-Process-Events innerhalb eines Application Context. Sie sind nützlich, um Module innerhalb derselben Anwendung zu entkoppeln. Kafka ist ein verteilter, persistenter Message Broker für Kommunikation zwischen Services, Retries, Backpressure und Event-Driven Architecture über Anwendungen hinweg. Spring-Events sind nicht durable und können verloren gehen, wenn die Anwendung abstürzt.


50. Kleine Code-Übung

Erstelle ein Event für Task-Abschluss.

Event:


public record TaskCompletedEvent(
Long taskId,
Long tenantId,
String title
) {
}

Publisher:


@Service
public class TaskService {

private final TaskRepository taskRepository;
private final ApplicationEventPublisher eventPublisher;

public TaskService(
TaskRepository taskRepository,
ApplicationEventPublisher eventPublisher
) {
this.taskRepository = taskRepository;
this.eventPublisher = eventPublisher;
}

@Transactional
public TaskDto complete(Long taskId) {
TaskEntity task = taskRepository.findById(taskId)
.orElseThrow();

task.complete();

eventPublisher.publishEvent(
new TaskCompletedEvent(
task.getId(),
task.getTenantId(),
task.getTitle()
)
);

return new TaskDto(task.getId(), task.getTitle(), task.getStatus());
}
}

Listener:


@Component
public class TaskCompletedAuditListener {

private final AuditService auditService;

public TaskCompletedAuditListener(AuditService auditService) {
this.auditService = auditService;
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void audit(TaskCompletedEvent event) {
auditService.recordTaskCompleted(
event.taskId(),
event.tenantId(),
event.title()
);
}
}

Frage:

Warum AFTER_COMMIT nutzen?

Antwort:


Weil der Audit-Nebeneffekt erst passieren soll, nachdem der Task-Abschluss erfolgreich committed wurde.

51. Kleine Bug-Übung 1

Problem:


@Transactional
public void registerUser(RegisterUserRequest request) {
UserEntity user = userRepository.save(...);

eventPublisher.publishEvent(new UserRegisteredEvent(user.getId(), user.getEmail()));

throw new RuntimeException("Something failed");
}

Listener:


@EventListener
public void sendEmail(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}

Frage:

Was ist gefährlich?

Antwort:

Der normale @EventListener kann laufen, bevor die Transaktion committed wird. Die E-Mail kann gesendet werden, obwohl die Transaktion später zurückrollt.

Besser:


@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)

52. Kleine Bug-Übung 2

Problem:


@Async
@EventListener
public void handle(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}

Aber es läuft trotzdem synchron.

Frage:

Was fehlt möglicherweise?

Antwort:

@EnableAsync fehlt möglicherweise.


53. Kleine Bug-Übung 3

Problem:


public record UserRegisteredEvent(UserEntity user) {
}

Frage:

Warum ist das nicht ideal?

Antwort:

Eine JPA-Entity in einem Event kann Lazy-Loading-Probleme, Detached-Entity-Probleme, versehentliche Änderungen, große Payloads und verwirrende Transaktionsgrenzen verursachen. Bevorzuge IDs und einfache Werte.


54. Kleine Bug-Übung 4

Problem:


public record SendWelcomeEmailEvent(String email) {
}

Frage:

Warum ist dieser Event-Name nicht ideal?

Antwort:

Er benennt die Aktion, die ein Listener ausführen soll, nicht den passierten Fakt. Besser:


public record UserRegisteredEvent(Long userId, String email) {
}

Übungsfragen

Frage 1

Was ist ein Spring-Event?

Antwort:

Ein Spring-Event ist ein Objekt, das innerhalb des Application Context veröffentlicht wird, um zu signalisieren, dass etwas passiert ist.


Frage 2

Warum Events nutzen?

Antwort:

Events reduzieren die direkte Kopplung zwischen der Haupt-Geschäftsaktion und Nebeneffekten oder Reaktionen.


Frage 3

Was macht ApplicationEventPublisher?

Antwort:

ApplicationEventPublisher veröffentlicht Events, indem du publishEvent(...) aufrufst.


Frage 4

Was macht @EventListener?

Antwort:

@EventListener markiert eine Spring-Bean-Methode als Listener für einen passenden Event-Typ.


Frage 5

Sind Spring-Events standardmäßig synchron?

Antwort:

Ja. Standardmäßig sind Spring-Event-Listener synchron.


Frage 6

Was passiert, wenn ein synchroner Listener eine Exception wirft?

Antwort:

Die Exception kann zum Publisher zurückpropagieren und die Publisher-Operation zum Fehlschlagen bringen.


Frage 7

Warum kann ein normaler @EventListener innerhalb einer Transaktion gefährlich sein?

Antwort:

Weil der Listener laufen kann, bevor die Transaktion committed wird. Wenn die Transaktion später zurückrollt, sind Nebeneffekte möglicherweise schon passiert.


Frage 8

Was macht @TransactionalEventListener?

Antwort:

Es bindet die Listener-Ausführung an eine Transaktionsphase.


Frage 9

Was bedeutet AFTER_COMMIT?

Antwort:

Der Listener läuft, nachdem die Transaktion erfolgreich committed wurde.


Frage 10

Was bedeutet AFTER_ROLLBACK?

Antwort:

Der Listener läuft, nachdem die Transaktion zurückgerollt wurde.


Frage 11

Was bedeutet fallbackExecution = true?

Antwort:

Es erlaubt dem transaktionalen Event-Listener zu laufen, auch wenn keine Transaktion existiert.


Frage 12

Wie mache ich einen Event-Listener asynchron?

Antwort:

Annotiere den Listener mit @Async.


Frage 13

Was braucht @Async?

Antwort:

Async-Support muss mit @EnableAsync aktiviert sein, und der Listener muss ein Spring Bean sein.


Frage 14

Sind Spring-Events durable Messaging?

Antwort:

Nein. Spring-Events sind lokale In-Process-Events und kein durable Messaging.


Frage 15

Wann solltest du Kafka/RabbitMQ statt Spring-Events nutzen?

Antwort:

Nutze einen Broker, wenn die Kommunikation zwischen Services läuft, durable sein muss, wiederholt werden muss oder Anwendungsabstürze überleben soll.


Frage 16

Was sollen Event-Namen beschreiben?

Antwort:

Event-Namen sollten beschreiben, was passiert ist — zum Beispiel UserRegisteredEvent, nicht was getan werden soll.


Frage 17

Was soll eine Event-Payload enthalten?

Antwort:

Kleine unveränderliche Daten wie IDs, Tenant-IDs, E-Mail, Status, Timestamp oder andere Geschäftsfakten.


Frage 18

Warum JPA-Entities in Events vermeiden?

Antwort:

Weil JPA-Entities Lazy Loading, Detached-Entity-Probleme, versehentliche Änderungen, große Payloads und Transaktionsgrenzen-Probleme verursachen können.


Frage 19

Wie teste ich Event-Veröffentlichung?

Antwort:

Mocke ApplicationEventPublisher, rufe den Service auf, fange das veröffentlichte Event mit ArgumentCaptor ab und prüfe seine Felder.


Frage 20

Wie teste ich Listener-Logik?

Antwort:

Instanziiere den Listener mit gemockten Abhängigkeiten, rufe die Listener-Methode direkt auf und prüfe dann den erwarteten Nebeneffekt.

Merksätze zum Mitnehmen

  • Spring-Events signalisieren, dass etwas passiert ist.
  • ApplicationEventPublisher veröffentlicht Events.
  • @EventListener empfängt Events.
  • Ein Event kann viele Listener haben.
  • Standardmäßig sind Spring-Events synchron.
  • publishEvent() wartet auf synchrone Listener.
  • Listener-Exceptions können den Publisher beeinflussen.
  • Ein normaler @EventListener kann laufen, bevor die Transaktion committed wird.
  • @TransactionalEventListener bindet die Listener-Ausführung an eine Transaktionsphase.
  • AFTER_COMMIT läuft nach erfolgreichem Commit.
  • AFTER_ROLLBACK läuft nach Rollback.
  • fallbackExecution = true läuft auch ohne Transaktion.
  • @Async kann Listener asynchron machen.
  • @EnableAsync wird für Async-Support benötigt.
  • Asynchrone Listener laufen in einem anderen Thread-Kontext.
  • Spring-Events sind In-Process, kein durable Messaging.
  • Nutze Kafka/RabbitMQ/Outbox für durable verteilte Events.
  • Benenne Events nach Fakten, nicht nach Befehlen.
  • Halte Event-Payloads klein und unveränderlich.
  • Bevorzuge IDs und einfache Werte statt JPA-Entities.
  • Direkter Aufruf für erforderliche Arbeit; Event für Reaktionen.