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:
- Ein Benutzer hat sich registriert
- Eine Bestellung ist eingegangen
- Die Zahlungsfrist ist abgelaufen
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:
Für den Einsatz von Event Sourcing werden typischerweise folgende Vorteile angeführt:
- Die gespeicherten Events beschreiben nicht nur den aktuellen Zustand, sondern auch, wie dieser Zustand entstanden ist.
- Es ist jederzeit möglich, einen beliebigen Zustand aus der Vergangenheit herzustellen, indem die Events nur bis zu einem bestimmten Zeitpunkt wieder abgespielt werden.
- Es ist denkbar, mit Event Sourcing eine falsche Verarbeitung von früheren Ereignissen oder das Eintreffen eines verspäteten Events zu behandeln.
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:
Der Prozess für die Registrierung läuft dabei so ab:
- Der Benutzer gibt in der Mobile-App seine Telefonnummer ein.
- Der Benutzer erhält eine SMS mit einem Code für die Bestätigung seiner Telefonnummer.
- Der Benutzer gibt den Bestätigungscode ein.
- Der Benutzer gibt die restlichen Daten wie z.B. seinen Namen oder seine Adresse an und schliesst die Registrierung 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:
-
MobileNumberProvided
(MobileNumber
) -
VerificationCodeGenerated
(VerificationCode
) -
MobileNumberValidated
(kein Zustand) -
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
diese feingranularen Events verarbeiten, und dabei zumindest einen Teil der Logik aus dem Aggregate
UserRegistration
kennen (z.B. ab welchem Event ist ein Benutzer wirklich vollständig registriert).mehrere Events kombinieren, um den gesamten benötigen Zustand über den Benutzer zu erfahren (z.B. die Telefonnummer aus
MobileNumberProvided
und die weiteren Details ausUserDetailsProvided
)einzelne Events ignorieren, welche im jeweiligen Bounded Context nicht von Interesse sind (z.B.
VerificationCodeGenerated
oderMobileNumberValidated
zur Bestätigung der Telefonnumer)
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.