Was haben Domain Events mit Event Sourcing gemeinsam? Sicherlich mal das Wort “Event” im Namen. Aber auch darüber hinaus höre ich im Austausch mit Architekten und Entwicklern in Projekten, an Konferenzen und Trainings immer mal wieder, dass Domain Events gut mit Event Sourcing zusammenpassen, bzw. dass Event Sourcing eine ideale Quelle von Domain Events darstellt. In diesem Blog-Post möchte ich aufzeigen, weshalb ich persönlich diese Ansicht nicht teile.

(Read this blog post in English)

Bevor ich argumentieren möchte, weshalb ich diese Ansicht nicht teile, möchte ich ein ausreichendes Verständnis zu Domain Events und Event Sourcing schaffen:

Domain Events

In Domain-driven Design sind Domain Events beschrieben als etwas, was in der Domäne passiert und für Fachexperten wichtig ist, also ein fachliches Ereignis. Solche Ereignisse passieren typischerweise unabhängig davon, ob bzw. wie stark die jeweilige Domäne in einem Software-System abgebildet ist. Sie sind auch unabhängig von Technologien. Entsprechend besitzen Domain Events eine hochwertige fachliche Semantik, welche in der von Fachexperten gesprochenen Sprache ausgedrückt wird. Beispiele können sein:

Domain Events sind sowohl innerhalb eines Bounded Contexts, als auch über Bounded Contexts hinweg relevant, um fachliche Abläufe abzubilden. Domain Events eignen sich auch hervorragend, um andere Bounded Contexts über bestimmte fachliche Ereignisse zu informieren, welche im eigenen Bounded Context aufgetreten sind und so mehrere Bounded Contexts ereignisgetrieben zu integrieren.

Event Sourcing

Martin Fowler beschreibt die aus meiner Sicht entscheidende Eigenschaft von Event Sourcing in seinem ursprünglichen Blog-Post wie folgt:

Event Sourcing ensures that all changes to application state are stored as a sequence of events.

Statt also den aktuellen Zustand innerhalb der Applikation direkt Feld um Feld in einer Tabelle in der Datenbank abzulegen und bei Bedarf wieder zu laden und durch nachfolgende Änderungen wieder zu überschreiben, wird eine chronologisch geordnete Liste von Events persistiert, welche danach verwendet werden kann, um bei Bedarf den aktuellen Zustand im Speicher wieder herzustellen.

Event Sourcing ist ein allgemeines Konzept, wird aber im Kontext von Domain-driven Design häufig im Zusammenhang mit Aggregates genannt. Daher verwende ich hier die Persistenz von Aggregates als Beispiel für die Verwendung von Event Sourcing.

Folgende Sequenz visualisiert die relevanten Schritte beim Einsatz von Event Sourcing für das Persistieren und Wiederherstellen von Zustand eines Aggregate:

Eine fachliche Aktion soll auf einem bestehenden Aggregate angestossen werden. Für dieses Aggregate sind bereits zwei frühere Events persistiert worden.
Eine fachliche Aktion soll auf einem bestehenden Aggregate angestossen werden. Für dieses Aggregate sind bereits zwei frühere Events persistiert worden.
Vor der Verarbeitung des Aufrufs wird eine leere Instanz des Aggregates erzeugt und die persistierten Events werden erneut auf dem Aggregate abgespielt. Dabei liest das Aggregate lediglich den Zustand aus den jeweiligen Events ein, es wird dabei keine Fachlogik ausgeführt. Danach hat das Aggregate wieder seinen aktuellen Zustand im Speicher.
Vor der Verarbeitung des Aufrufs wird eine leere Instanz des Aggregates erzeugt und die persistierten Events werden erneut auf dem Aggregate abgespielt. Dabei liest das Aggregate lediglich den Zustand aus den jeweiligen Events ein, es wird dabei keine Fachlogik ausgeführt. Danach hat das Aggregate wieder seinen aktuellen Zustand im Speicher.
Der Aufruf wird vom Aggregate angenommen, gegen seinen aktuellen Zustand validiert und verarbeitet, d.h. die jeweilige Fachlogik wird durchlaufen. Dabei wird der interne Zustand des Aggregates vorerst nicht verändert. Dies wird erst später durch die Verarbeitung des durch die Aktion entstandenen Events erfolgen.
Der Aufruf wird vom Aggregate angenommen, gegen seinen aktuellen Zustand validiert und verarbeitet, d.h. die jeweilige Fachlogik wird durchlaufen. Dabei wird der interne Zustand des Aggregates vorerst nicht verändert. Dies wird erst später durch die Verarbeitung des durch die Aktion entstandenen Events erfolgen.
Das Aggregate erzeugt als Konsequenz der Verarbeitung des Aufrufs einen Event (oder auch mehrere), welcher das Ereignis im Aggregate beschreibt, inklusive des Zustands, der für die spätere Wiederherstellung des Zustands im Aggregate benötigt wird. Der Event wird persistiert, so dass er bei einem zukünftigen weiteren Aufruf auf dieses Aggregate verwendet werden kann, um den aktuellen Zustand wieder herzustellen.
Das Aggregate erzeugt als Konsequenz der Verarbeitung des Aufrufs einen Event (oder auch mehrere), welcher das Ereignis im Aggregate beschreibt, inklusive des Zustands, der für die spätere Wiederherstellung des Zustands im Aggregate benötigt wird. Der Event wird persistiert, so dass er bei einem zukünftigen weiteren Aufruf auf dieses Aggregate verwendet werden kann, um den aktuellen Zustand wieder herzustellen.

Für den Einsatz von Event Sourcing werden typischerweise folgende Vorteile angeführt:

Gleichzeitig bringt die Umsetzung von Event Sourcing auch eine gewisse konzeptionelle und technische Komplexität mit sich. Es muss damit umgegangen werden, dass sich Events nachträglich nicht mehr verändern sollen, die Fachlichkeit hingegen sich oftmals weiterentwickelt. Somit muss der Code auch sehr alte Events noch verarbeiten können. Um bei langen Historien von Events dennoch performant den Zustand erneut herstellen zu können, sind Snapshots notwendig.

Auch die Umsetzung von Anforderungen z.B. aus der Datenschutz-Grundverordnung der EU (EU-DSVGO) stellt bei Event Sourcing eine echte Herausforderung dar, da einmal persistierte Events für das korrekte Funktionieren von Event Sourcing auch nicht mehr einfach einzeln gelöscht werden können.

Events aus Event Sourcing ≠ Domain Events

Weshalb bin ich nun der Meinung, dass diese beiden Konzepte nicht so wirklich natürlich zusammenpassen?

Betrachten wir folgendes Beispiel: in einer Domäne für Bike Sharing will sich ein Benutzer registrieren, damit er im Anschluss ein Fahrrad ausleihen und fahren kann. Natürlich muss er dafür auch etwas bezahlen, was über einen Pre-Paid-Ansatz mit einem Wallet erfolgt.

Der relevante Ausschnitt der Context Map für diese Domäne könnte dabei wie folgt aussehen:

Registrierung in Bike Sharing Domain
Registrierung in Bike Sharing Domain

Der Prozess für die Registrierung läuft dabei so ab:

Dieser Prozess wird in der Umsetzung auf dem Aggregate UserRegistration im Bounded Context Registration abgebildet. Der Benutzer interagiert über den Verlauf der Registrierung also mehrere Mal mit “seiner” Instanz der UserRegistration. Dabei baut sich der Zustand der UserRegistration Schritt für Schritt auf, bis die Registrierung schlussendlich erfolgreich abgeschlossen wird. Ab dann soll der Benutzer in der Lage sein, Guthaben auf sein Wallet zu laden und damit ein Fahrrad zu mieten.

Wird nun Event Sourcing verwendet, um den Zustand des Aggregate UserRegistration zu verwalten, entstehen über die Zeit folgende Events (mit dem jeweils relevanten Zustand) welche persistiert werden:

  1. MobileNumberProvided (MobileNumber)
  2. VerificationCodeGenerated (VerificationCode)
  3. MobileNumberValidated (kein Zustand)
  4. UserDetailsProvided (FullName, Address, …)

Diese Events reichen aus, um den jeweils aktuellen Zustand des Aggregats UserRegistration jederzeit wieder aufbauen zu können. Weitere Events sind nicht notwendig, inbesondere kein Event, der ausdrücken würde, dass die Registrierung nun abgeschlossen ist. Dies ist aufgrund der Fachlogik im Aggregate UserRegistration klar, sobald das Ereignis UserDetailsProvided verarbeitet wurde. Entsprechend könnte eine Instanz einer UserRegistration auch zu jedem Zeitpunkt beantworten, ob die Registrierung bereits abgeschlossen ist.

Zudem enthält jeder Event nur denjenigen Zustand, der notwendig ist, um beim Wiederabspielen den Zustand des Aggregate erneut aufbauen zu können. Dies ist typischerweise nur der Zustand, der beim Aufruf, welcher den Event ausgelöst hat, fachlich beeinflusst wurde, also eine Art “Diff”. Aus Sicht von Event Sourcing ergibt es keinen Sinn, auf einem Event zusätzlichen Zustand abzulegen, der vom Aufruf nicht beeinflusst wurde. Selbst wenn also zusätzlich ein expliziter Event UserRegistrationCompleted persistiert würde, hätte dieser keinen zusätzlichen Zustand abgelegt.

Einige Verfechter von Event Sourcing votieren nun, dass genau diese Events aus Event Sourcing für das Aggregate UserRegistration auch an andere Interessenten innerhalb oder auch ausserhalb des Bounded Contexts publiziert werden können und dadurch weitere fachliche Ereignisse auslösen oder Zustand aktualisieren können. In unserem Beispiel wären dies die beiden Bounded Contexts Accounting (für die Initialisierung des Wallets) und Rental (für das Speichern des registrierten Benutzers).

Soll dies nun anhand der Events aus Event Sourcing erfolgen, muss jeder konsumierende Bounded Context

Dieser Ansatz bricht aus meiner Sicht die angestrebte Kapselung zwischen unterschiedlichen fachlichen Teilen des Systems, führt zu vergleichsweise viel Kommunikation zwischen Bounded Contexts und erhöht somit die Kopplung zwischen Bounded Contexts. Der Hauptgrund dafür ist, dass die Semantik der feingranularen Events aus Event Sourcing nicht hochwertig genug ist, sowohl in Bezug auf das Ereignis selbst, als auch die damit verbundenen Informationen (der “Payload”).

Meiner Meinung nach viel besser wäre, wenn das Aggregate UserRegistration auch beim Einsatz von Event Sourcing beim erfolgreichen Abschluss der Registrierung zusätzlich einen Domain Event UserRegistrationCompleted mit den relevanten Informationen MobileNumber, FullName und Address (aber z.B. nicht VerificationCode) als Payload publizieren würde. Dieser Domain Event verfügt über die passende Semantik, um von externen Bounded Contexts einfach verarbeitet werden zu können, ohne dass diese die Internas des Registrierungsprozesses kennen müssen.

Es ist sicherlich denkbar, dass die Semantik eines Events aus Event Sourcing in einigen Fällen ausreichende Semantik bietet, um von einem externen Konsumenten sinnvoll verarbeitet zu werden (z.B. der Event MobileNumberProvided für einen hypothetischen Konsument, der alle verwendeten Telefonnummern kennen will), jedoch empfiehlt es sich aus meiner Sicht auch hier, die Umsetzung der Events für Event Sourcing und die Domain Events zu trennen, so dass sie sich unabhängig voneinander entwickeln können. Konkret bedeutet dies, dass es im System zwei Repräsentationen des fachlichen Ereignis Telefonnummer wurde eingegeben gibt, mit jeweils unterschiedlichem Einsatzzweck.

Event Sourcing und CQRS

Sollten Events aus Event Sourcing also nur innerhalb des jeweiligen Aggregate benutzt werden?

Aus meiner Sicht pinzipiell ja. Eine mögliche und sinnvolle Ausnahme kann allerdings die Verwendung dieser Events im Zusammenhang mit dem Aufbau von Read-Modellen bei CQRS sein. Auch dies hat natürlich einen Einfluss auf die Kapselung, aber erfahrungsgemäss sind Read-Modelle aus CQRS oft relativ eng mit dem jeweiligen (Haupt-)Aggregate verbunden, da sie eine bestimmte Sicht auf Daten aus dem Aggregate bereitstellen. Somit kann man argumentieren, dass die durch die Verarbeitung der feingranularen Events im Read-Modell entstehende Kopplung akzeptabel ist.

Fazit

Ich verstehe Event Sourcing als eine Implementationsstrategie für die Persistenz von Zustand, z.B. von Aggregates. Diese Strategie sollte nicht über das Aggregate hinaus exponiert werden. Die dabei entstehenden Events sollten somit nur intern im jeweiligen Aggregate oder im Kontext von CQRS für den Aufbau von verwandten Read-Modellen verwendet werden.

Domain Events repräsentieren hingegen ein bestimmtes fachliches Ereignis, das unabhängig von der Art der Persistenz von Aggregaten relevant ist, eben z.B. für die Integration von Bounded Contexts.

Event Sourcing und Domain Events können somit gleichzeitig eingesetzt werden, beeinflussen sich aber nicht. Die beiden Konzepte werden für unterschiedliche Zwecke eingesetzt und sollten entsprechend nicht miteinander vermischt werden.