Von Silas Graffy

Estimated reading time: 10 Minuten

Techblog

Event Sourcing, Part 1/2: Was es (nicht) mit CQRS zu tun hat und andere Missverständnisse

Event Sourcing ist in aller Munde, CQRS auch. Gehört ja auch zusammen, oder? Hört man – finde ich – viel zu oft und deshalb starte ich heute eine Miniserie aus zwei Blog-Artikeln über Event Sourcing. Teil 1 erläutert, was Event Sourcing überhaupt ist, grenzt es von CQRS ab und gibt Modellierungsbeispiele. In Teil 2 geht es…

Techblog

Event Sourcing ist in aller Munde, CQRS auch. Gehört ja auch zusammen, oder? Hört man – finde ich – viel zu oft und deshalb starte ich heute eine Miniserie aus zwei Blog-Artikeln über Event Sourcing. Teil 1 erläutert, was Event Sourcing überhaupt ist, grenzt es von CQRS ab und gibt Modellierungsbeispiele. In Teil 2 geht es darum, für welche Anwendungsfälle sich Event Sourcing eignet und welche Möglichkeiten zur Implementierung der Persistenzschicht existieren.

Was Event Sourcing ist …

Event Sourcing ist ein Persistenzmechanismus, bei dem der aktuelle Zustand einer Entität nicht direkt gespeichert wird, sondern in Form einer chronologischen Liste all seiner Änderungen (Domain Events) beginnend mit der Erstellung, dem sogenannten Event Stream. Das Prinzip kennen wir aus der Buchhaltung: Bei einem Konto wird der Zustand des Objekts (also z.B. der Kontostand) aus den einzelnen Änderungen (den Buchungen) wiederhergestellt. Ausführlicher erläutere ich das am Ende dieses ersten Artikels, ungeduldige Leser scrollen einfach schon mal vor zur Abbildung.

… und was nicht

In den letzten Jahren werden Event Sourcing und CQRS (Command Query Responsibility Segregation) zunehmend populärer. Das hängt sicher damit zusammen, dass beide gut mit reichhaltigen Domain Models harmonieren, wie wir sie aus dem Domain-Driven Design kennen. Diese Art der Modellierung wiederrum erfährt im Zuge der Microservice-Bewegung starkes Momentum. Aber was ist CQRS eigentlich und was hat es mit Event Sourcing zu tun?

CQRS ist ein auf Greg Young zurückgehendes Architekturmuster, bei dem in einer Anwendung die Verantwortlichkeiten für Lese- und Schreibzugriffe verschiedenen Modellen obliegen. Man spricht je nach der Intention vom Read Model oder dem Write Model. Es ist eine Umsetzung des bereits zuvor bekannten Prinzips der Command-query separation (CQS) von Bertrand Mayer, nach dem Methoden entweder einen Rückgabewert haben (nebeneffektfreie Queries) oder einen Zustand verändern (Commands), nicht aber beides. 

Eine beliebte Umsetzungsvariante von CQRS ist es, das Write Model mittels Event Sourcing zu speichern und ein oder mehrere Read Models mit den jeweils aktuellsten Entitätszuständen in einer relationalen Datenbank vorzuhalten. Solche Read Models haben dann den Vorteil, dass sie denormalisiert und für bestimmte Zugriffsmuster, z. B. häufige Suchanfragen, optimiert sind. 

Die Kombination von CQRS mit Event Sourcing ist aber keineswegs zwingend. Genauso gut könnte das Write Model ein stark normalisiertes relationales Datenbankschema sein und das Read Model ein denormalisiertes relationales Datenbankschema. Ich möchte an dieser Stelle gar nicht weiter auf CQRS eingehen. Daher sei die interessierte Leserin, die sich z. B. fragt, wie nun Read und Write Model synchron gehalten werden und wann sich der ganze Aufwand überhaupt lohnt, auf die Links zum Abschluss der Serie verwiesen.

Häufige Missverständnisse über Event Sourcing

Bevor wir uns näher mit der Modellierung mittels Events beschäftigen, möchte ich jedoch mit dem Ausräumen einiger geläufiger Missverständnisse bezüglich Event Sourcing beginnen:

1. Event Sourcing ist ein Architekturstil.

Nein, es ist “nur” ein Persistenzmechanismus (s. o.), der allerdings einige Implikationen für das Domain Model hat. Schließlich müssen die Entitäten bei Änderungen Events emittieren, die dann gespeichert werden können. Auch stellt sich die Frage, wie man denn zur Laufzeit die Instanz einer Domain Entity erstellt. In unserem aktuellen Projekt haben wir uns dafür entschieden, den jeweiligen Klassen nur einen einzigen Konstruktor mit dem Event Stream als einzigem Parameter zu geben. Das Ganze lässt sich mit verschiedenen anderen Architekturmustern kombinieren, wir setzen auf Ports and Adapters.

2. Event Sourcing funktioniert nur mit CQRS.

Nein, auch wenn diese Kombination häufig sinnvoll ist, muss der Aufwand für CQRS nicht immer erforderlich sein – gerade wenn die Anwendung nur wenige Lesezugriffe zu verzeichnen hat. Gibt es kein zusätzliches Read Model, können Entitäten entweder nach ID abgerufen werden (dann wird einfach der entsprechende Event Stream abgespielt) oder die Liste aller Entitäten (dann müssen alle Event Streams abgespielt werden). Diese kann man nach Belieben filtern. Geht das dann doch nicht schnell genug, gibt es verschiedene Optimierungsmöglichkeiten, die noch vor einem dedizierten Read Model und CQRS kommen: Man kann beispielsweise den Event Stream im Speicher halten, sofern er hinreichend klein ist. Eine andere Variante wäre es, häufig benötigte Entities im Speicher zu halten. Alternativ kann man mit Snapshots arbeiten, die alle n Versionen einer Entität gebildet werden. Hat man beispielsweise alle 100 Versionen einen Snapshot und die aktuelle Version einer Entität ist 942, muss man statt aller 942 nur 42 Events verarbeiten, um sie einzulesen. Mehr zur Versionierung in Event Sourcing Domain Models folgt im Anschluss im Beispiel.

3. Die Event-Verarbeitung erfolgt asynchron.

Kann sein, muss aber nicht. Im einfachsten Fall gibt eine Operation auf einer Entität einfach das resultierende Event zurück. Der aufrufende Code kann es persistieren, fertig. Natürlich könnte das Event auch noch innerhalb der Anwendung über einen Event Bus publiziert werden oder über einen Message Broker über Systemgrenzen hinweg propagiert. Eine solche Event-Driven Architecture (EDA) setzt dann auf asynchrones Messaging. Ähnlich wie CQRS ergänzen sich EDA und Event Sourcing sehr gut, können aber auch völlig unabhängig voneinander eingesetzt werden.

Modellierung mit Domain Events im Beispiel

Eric Evans definiert Domain Events u. a. in seiner DDD Reference als 

Something happened that domain experts care about.

Die Betonung liegt dabei auf happened, die Vergangenheitsform ist wichtig. Daher sind (nicht nur Domain) Events – wie alles in der Vergangenheit – unveränderlich und sollten auch so implementiert werden (u. a. mittels public final Feldern und immutable Datentypen). Eine weitere Folge für die Implementierung besteht darin, dass alle Events eines Event Streams immer bedingungslos verarbeitet werden, sie sind ja bereits aufgetreten und es geht zu diesem Zeitpunkt nur noch darum, den daraus resultierenden Zustand einer Entität (wieder) herzustellen. 

Neben einem Timestamp und der ID der betreffenden Entität enthalten Events immer mindestens eine Versionsnummer. Diese entspricht der chronologischen Reihenfolge ihres Entstehens und damit auch ihrer Reihenfolge im Stream. 

Bankkonto als einfaches Beispiel

Als kanonisches Beispiel für eine Modellierung mit Domain Events dient normalerweise ein Bankkonto. Statt einfach nur einen Kontostand (plus ein paar Metainformationen) zu speichern, kann man ein Konto wie folgt modellieren: Ist das Konto eröffnet worden, handelt es sich um ein “Konto eröffnet”-Event. Attribute des “Konto eröffnet”-Events könnten der Konto-Inhaber sein, die Kontonummer oder IBAN, vielleicht auch die Bankleitzahl. Das Guthaben ist in dieser Version 1 des jeweiligen Kontos 0 €, Inhaber und Kontonummer kommen aus dem Event. Den Guthaben-Wert 0 € würde man vermutlich nicht als Attribut des Events modellieren, er ergibt sich ja bereits aus dessen Semantik. 

Erfolgen nun Einzahlungen oder Abhebungen, werden diese jeweils auch in Form von Events gespeichert. Neben dem Betreff und Zeitpunkt ist hier der Betrag von besonderem Interesse. Der aktuelle Kontostand ergibt sich dann jeweils aus dem Event Stream. Beispiel: Werden nach Kontoeröffnung zunächst 100 € eingezahlt und anschließend wieder 50 € abgehoben, entstehen drei Events:

  • Zunächst ein “Konto eröffnet”-Event. In Version 1 hat das Konto ein Guthaben von 0 €.
  • Dann ein “Einzahlung erfolgt”-Event mit einem Betrag von 100 €. Folglich hat das Konto in Version 2 ein Guthaben von 100 €.
  • Das dritte Event ist schließlich ein “Abhebung erfolgt”-Event mit einem Betrag von 50 €. In Version 3 weist das Konto daher nur noch ein Guthaben von 50 € auf. 

Weitere Events könnten “Überweisung erfolgt” oder “Scheck verrechnet” heißen, aber auch solche, die sich nicht auf den Kontostand auswirken sind denkbar: “Bevollmächtigter ergänzt”/”entfernt”, “Dispo-Limit geändert” oder auch “Konto geschlossen”. Durch solche “retroaktiven” Events werden Änderungen rückgängig gemacht, denn wirklich gelöscht werden Entitäten oder Events beim Event Sourcing in aller Regel nicht.

Geschäftsprozesse mit Zustandsübergängen als weiterer Anwendungsfall

In unserem aktuellen Kundenprojekt ist die Fachlichkeit auf abstrakter Ebene vergleichbar mit Bankkonten. Auch hier geht es um “Abbuchungen” und “Gutschriften” auf als (Vektoren von) Zahlen dargestellte “Guthaben”. Ergänzend kommt hier noch eine Art Antragsworkflow für jede “Buchung” hinzu. Auch die hierfür nötigen Zustandsübergänge im Workflow lassen sich gut mittels Events modellieren: Soll eine Buchung getätigt werden, tritt zunächst ein “Buchungsantrag erfasst”-Event auf, gegebenfalls gefolgt von einem oder mehreren “Buchungsantrag verändert”-Events. Solche lange laufenden fachlichen Transaktionen – der Antragsworkflow dauert in aller Regel mehrere Tage – werden häufig als Sagas bezeichnet.

Der eigentliche Zustandsübergang erfolgt dann durch ein “Buchung beantragt”-Event. Mögliche Verläufe beinhalten dann “Antrag bewilligt” und “Antrag abgelehnt”-Events ebenso wie “Antrag zur Überarbeitung”. Aus der Bewilligung resultiert dann ein “Buchung erfolgt”-Event, das den eigentlichen “Kontostand” aktualisiert. Konto und Buchungsantrag verfügen in diesem Modell über eigene Identifikatoren, das “Buchung erfolgt”-Event eines Kontos stellt die Verbindung her, da es als Attribut seinen Auslöser mitführt, das jeweilige “Antrag bewilligt”-Event.


Im zweiten Teil der Mini-Serie beschreibe ich, welche typischen Anwendungsfälle es für Event Sourcing gibt und wie man Events persistiert. 


Über den Autor

Von Silas Graffy

IT-Sanierung 

Silas stieß 2015, nach 15 Jahren Produktentwicklung & Forschung, zu MaibornWolff. Schwerpunkte des Informatikers aus Leidenschaft bilden Software-Architekturen, agile Softwareentwicklung und System- & Architektur-Audits. Software- und Code-Qualität sind ihm ebenso ein Herzensanliegen wie Teamkultur und Collective Ownership. Er hat Abschlüssse in angewandter und Wirtschaftsinformatik. In der Freizeit gelten für ihn die wichtigen 4C: Code, Coffee, Cocktails — and Climbing.