Softwarearchitektur verstehen und entscheiden

Aaron KreisÜber mich
Datenarchitektur

Event Sourcing

Zustand als Folge von Ereignissen speichern

Die Idee und ihr Nutzen

Die Versicherungsaufsicht fragt bei einer Versicherung nach, wie sich bei einem strittigen Haftpflichtschaden die Reserve entwickelt hat, also der Betrag, den die Versicherung für diesen Schaden zurücklegt. Die Versicherungsaufsicht will wissen, wer die Reserve wann auf welchen Wert gesetzt hat und welche Zahlungen vorausgingen. Ihre Schäden bearbeitet die Versicherung in der Schadenanwendung. Die Schadenanwendung speichert aber nur den heutigen Stand, „Schaden abgeschlossen, Reserve 0 Euro“, und kann die Frage nicht beantworten. Damit die Schadenanwendung solche Fragen beantworten kann, speichert sie jede fachliche Änderung als eigenes, unveränderliches Ereignis in zeitlicher Reihenfolge, etwa „3. März, Frau Berg: Reserve auf 20.000 Euro gesetzt“. Aus diesen Ereignissen berechnet sie den heutigen Stand und kann zugleich jeden Schritt bis zu diesem Stand zeigen. Diese Art zu speichern heißt Event Sourcing, auf Deutsch Speicherung aus Ereignissen. Der Name meint: Die Quelle für den heutigen Stand sind die gespeicherten Ereignisse, nicht eine Zeile mit dem heutigen Stand.

Bausteine: Versicherungsaufsicht: fragt nach der Entwicklung der Reserve; Schadenanwendung: berechnet den heutigen Stand beim Abspielen; Ereignisstrom Schaden 4711: unveränderlich, in zeitlicher Reihenfolge. Verbindungen: Versicherungsaufsicht — Schadenanwendung (wer wann welchen Betrag gesetzt hat); Schadenanwendung — Ereignisstrom Schaden 4711 (neues Ereignis); Schadenanwendung — Ereignisstrom Schaden 4711 (alle Ereignisse des Schadens). Die Schadenanwendung speichert jede Änderung als unveränderliches Ereignis und berechnet den heutigen Stand aus allen Ereignissen.

Wie funktioniert es?

Am 3. März setzt die Sachbearbeiterin Frau Berg die Reserve für den Haftpflichtschaden 4711 auf 20.000 Euro. Die Schadenanwendung überschreibt keinen gespeicherten Stand, sondern hängt ein Ereignis an den Ereignisstrom von Schaden 4711 an, also an die Folge aller Ereignisse dieses Schadens in zeitlicher Reihenfolge. Am 10. Mai erhöht Frau Bergs Kollege Herr Kurz die Reserve nach einem Gutachten auf 45.000 Euro. Die Schadenanwendung hängt ein zweites Ereignis an, und das erste Ereignis bleibt unverändert. Am 2. September wird eine Zahlung über 38.000 Euro ausgeführt, und die Schadenanwendung hängt sie als drittes Ereignis an. Am 5. September wird die Reserve aufgelöst, weil der Schaden abgeschlossen ist, und die Schadenanwendung hängt die Auflösung als viertes Ereignis an. Um den heutigen Stand zu zeigen, spielt die Schadenanwendung die vier Ereignisse ab: Sie beginnt bei null und wendet jedes Ereignis der Reihe nach an. So ergibt sich die Reserve 20.000 Euro, dann 45.000 Euro, dann nach Zahlung und Auflösung der Stand „abgeschlossen, Reserve 0 Euro“. Als die Versicherungsaufsicht nach der Entwicklung der Reserve fragt, liest das Team der Schadenanwendung den Ereignisstrom und belegt jeden Schritt mit Datum und verantwortlicher Person.

Beim Event Sourcing gibt es keine Zeile mit dem aktuellen Stand, die die Schadenanwendung bei jeder Änderung überschreibt. Den aktuellen Stand berechnet die Schadenanwendung immer aus dem Ereignisstrom. Ein Ereignis beschreibt, was geschehen ist, wann und durch wen, zum Beispiel „3. März, Frau Berg: Reserve auf 20.000 Euro gesetzt“. Es gibt einen Ereignisstrom je fachlichem Objekt, hier je Schaden. Muss etwas korrigiert werden, speichert die Schadenanwendung ein neues Ereignis wie „Reserve korrigiert“, statt das alte zu überschreiben. So sieht man später auch, welcher Betrag zuerst falsch eingetragen war.

Die Schadenanwendung ist das einzige Programm, das Reserven ändert. Bei sehr langen Ereignisströmen speichert sie gelegentlich einen Zwischenstand, englisch Snapshot. Beim Abspielen beginnt sie dann beim Zwischenstand und wendet nur die Ereignisse an, die danach hinzukamen. Ein Zwischenstand verkürzt nur das Abspielen und lässt sich jederzeit aus den Ereignissen neu berechnen. Für Abfragen über viele Schäden baut die Schadenanwendung aus den Ereignissen eigene Tabellen auf, die nur dem Lesen dienen. Diese Tabellen heißen Lesetabellen. Eine Lesetabelle enthält zum Beispiel die Liste aller offenen Schäden. Andere Programme lesen nur die Lesetabellen.

Event Sourcing macht nur die Änderungen nachvollziehbar, die tatsächlich als Ereignis gespeichert wurden. Ändert ein Programmteil Daten am Ereignisstrom vorbei, fehlt diese Änderung in der Geschichte, und die Antwort an die Versicherungsaufsicht ist nicht mehr lückenlos.

Event Sourcing: jeder Schritt der Reserve bleibt belegbar

Beteiligte: Frau Berg: setzt die erste Reserve; Herr Kurz: erhöht die Reserve nach einem Gutachten; Schadenanwendung: hängt Ereignisse an und spielt sie ab; Ereignisstrom Schaden 4711: unveränderliche Ereignisse in zeitlicher Reihenfolge; Versicherungsaufsicht: fragt nach der Entwicklung der Reserve. Ablauf: 1. Am 3. März setzt die Sachbearbeiterin Frau Berg die Reserve für Schaden 4711 auf 20.000 Euro. 2. Die Schadenanwendung überschreibt keinen gespeicherten Stand, sondern hängt ein Ereignis an den Ereignisstrom von Schaden 4711 an. 3. Am 10. Mai erhöht der Sachbearbeiter Herr Kurz die Reserve nach einem Gutachten auf 45.000 Euro. 4. Die Schadenanwendung hängt ein zweites Ereignis an. Das erste Ereignis bleibt unverändert. 5. Am 2. September wird eine Zahlung über 38.000 Euro ausgeführt. Die Schadenanwendung hängt sie als drittes Ereignis an. 6. Am 5. September wird die Reserve aufgelöst, weil der Schaden abgeschlossen ist. Die Schadenanwendung hängt die Auflösung als viertes Ereignis an. 7. Die Schadenanwendung spielt die vier Ereignisse der Reihe nach ab und erhält den heutigen Stand „abgeschlossen, Reserve 0 Euro“. 8. Die Versicherungsaufsicht fragt, wie sich die Reserve von Schaden 4711 entwickelt hat. 9. Das Team der Schadenanwendung belegt jeden Schritt mit den vier Ereignissen samt Datum und Person. Lückenlos ist das nur, wenn wirklich jede Änderung als Ereignis gespeichert wurde.

Wann ist es sinnvoll und wann nicht?

Event Sourcing lohnt sich

  • Die Geschichte der Änderungen ist fachlich wichtig. Beispiele sind Buchungen, Reserven und rechtlich bedeutsame Vorgänge. Speichert die Schadenanwendung nur eine Zeile je Schaden, überschreibt sie dabei jedes Mal den alten Wert. Aus der heutigen Zeile „Reserve 0 Euro, Schaden abgeschlossen“ ist dann nicht mehr zu erkennen, dass die Reserve zwischendurch 20.000 und dann 45.000 Euro betrug, wer sie geändert hat und warum.
  • Die Geschichte muss lückenlos sein. Eine Änderungsgeschichte in eigenen Tabellen muss jeder Programmteil, der Daten ändert, zusätzlich zum eigentlichen Speichern schreiben. Vergisst ein Programmteil das, fehlt ein Schritt, ohne dass das auffällt. Für eine Prüfung durch die Versicherungsaufsicht ist eine lückenhafte Geschichte wertlos. Beim Event Sourcing sind das Speichern und das Mitschreiben der Geschichte derselbe Vorgang: Eine Änderung, die als Ereignis gespeichert ist, steht damit auch in der Geschichte.
  • Später könnten neue Auswertungen über vergangene Abläufe gewünscht sein. Ein Beispiel ist die Frage, wie oft Reserven nach einem Gutachten erhöht werden. Ohne Event Sourcing sind die alten Werte überschrieben, und die Auswertung gelingt erst für Schäden ab dem Tag, an dem jemand die Daten dafür zu sammeln beginnt. Weil alle Ereignisse erhalten bleiben, lässt sich das auch für Schäden der vergangenen Jahre berechnen, obwohl beim Speichern niemand an diese Auswertung gedacht hat.
  • Die Anwendung nutzt CQRS oder soll es nutzen. CQRS trennt das Speichern von Änderungen vom Lesen in aufbereiteten Tabellen. Ohne Event Sourcing braucht die Anwendung für CQRS einen eigenen Weg, der jede Änderung an die aufbereiteten Tabellen weitergibt. Eine Anwendung mit Event Sourcing baut solche Tabellen ohnehin als Lesetabellen aus den Ereignissen auf. Event Sourcing verlangt CQRS aber nicht.

Event Sourcing ist nicht das Richtige

  • Nur der aktuelle Stand zählt. Ein Beispiel ist die Pflege einer Liste von Filialadressen. Wann eine Filiale umgezogen ist, fragt niemand nach, und ein Ereignisstrom würde Ereignisse aufbewahren, die niemand liest. Dann speichert das Programm je Filiale eine Zeile und überschreibt sie bei jeder Änderung. Ereignisströme, Abspielen und Lesetabellen entfallen.
  • Es muss nur nachvollziehbar sein, wer wann welches Feld geändert hat, etwa bei den Kontaktdaten eines Kunden, und die Versicherungsaufsicht prüft diese Geschichte nicht. Dafür braucht es keine fachlichen Ereignisse wie „Reserve erhöht“ und kein Abspielen. Dann schreibt die Anwendung eine Änderungsgeschichte in eigene Tabellen neben der Zeile mit dem aktuellen Stand. Die Anwendung schreibt Stand und Geschichte dann in derselben Datenbanktransaktion, also in einem Schreibvorgang, der nur ganz oder gar nicht gelingt. So kann ein Absturz nicht dazu führen, dass nur eine der beiden Angaben gespeichert ist. Abfragen, die nur den heutigen Stand brauchen, lesen wie gewohnt die Zeile, und Ereignisströme, Abspielen und Lesetabellen entfallen.
  • Das Team hat keine Erfahrung mit Event Sourcing, und der Zeitplan ist eng. Der Einstieg ist anspruchsvoll, weil das Team jede Änderung als fachliches Ereignis benennen und den Stand daraus berechnen muss, statt nur eine Zeile zu überschreiben. Dann speichert das Team den aktuellen Stand wie gewohnt in einer Zeile und schreibt, wo nötig, eine Änderungsgeschichte in eigene Tabellen. Das Benennen der Ereignisse, das Abspielen und die Lesetabellen entfallen.
  • Jede Abfrage über viele Schäden muss sofort den neuesten Stand zeigen. Solche Abfragen gehen nur über die Lesetabellen, und eine Änderung erscheint in den Lesetabellen erst kurz nachdem die Schadenanwendung das Ereignis angehängt hat. Dann speichert die Anwendung den aktuellen Stand in gewöhnlichen Tabellen und ändert sie bei jeder Änderung direkt. Jede Abfrage liest so den neuesten Stand. Wird die Geschichte trotzdem gebraucht, schreibt die Anwendung sie in derselben Datenbanktransaktion in eigene Tabellen.
  • Es sollen auch Lesezugriffe und abgelehnte Versuche festgehalten werden. Ein Ereignisstrom erfasst nur fachliche Änderungen und ersetzt kein Prüfprotokoll. Dann hält die Anwendung jeden Lesezugriff und jeden abgelehnten Versuch nach dem Ansatz Audit Logging in einem geschützten Protokoll fest. Einen Ereignisstrom braucht es dafür nicht.

Nachteile und typische Fehler

Ändert sich der Aufbau eines Ereignisses, muss die Schadenanwendung die alten Ereignisse weiterhin lesen können. Ein Beispiel ist ein neues Feld, das in den alten Ereignissen fehlt. Dafür braucht es Versionsnummern und Umwandlungsregeln, die alte Ereignisse in den neuen Aufbau überführen. Das Team der Schadenanwendung muss beides dauerhaft pflegen. Die Lesetabellen müssen aufgebaut, betrieben und bei Fehlern neu berechnet werden. Personenbezogene Daten lassen sich aus unveränderlichen Ereignissen nicht löschen, obwohl der Datenschutz das verlangen kann. Eine Möglichkeit ist, solche Daten in einem eigenen Speicher außerhalb des Ereignisstroms abzulegen und aus diesem Speicher zu löschen. Eine andere ist, diese Daten mit einem eigenen Schlüssel je Person zu verschlüsseln und diesen Schlüssel bei Bedarf zu vernichten. Ohne den Schlüssel sind die Daten nicht mehr lesbar. Bei dieser Lösung dürfen die Daten aber auch in Kopien, Lesetabellen und Sicherungen nicht unverschlüsselt stehen. Speichert die Schadenanwendung zu allgemeine Ereignisse, etwa „Schaden gespeichert“ statt „Reserve erhöht“, lässt sich das später schwer korrigieren, weil die alten Ereignisse erhalten bleiben.

Typische Fehler und woran man sie erkennt:

  • Änderung am Ereignisstrom vorbei: Ein Entwickler baut in die Schadenanwendung eine Korrekturfunktion ein, die einen Betrag direkt in der Lesetabelle der offenen Schäden ändert, ohne ein Ereignis anzuhängen. Die Änderung fehlt dann in der Geschichte. Baut die Schadenanwendung die Lesetabelle später aus den Ereignissen neu auf, verschwindet die Änderung. Erkennbar ist das an Abweichungen zwischen einer Lesetabelle und dem abgespielten Ereignisstrom.
  • Unlesbare alte Ereignisse: Das Team der Schadenanwendung gibt dem Ereignis „Reserve gesetzt“ ein neues Pflichtfeld für die Begründung, ohne Versionsnummer und Umwandlungsregel. Den alten Ereignissen fehlt dieses Feld, und die Schadenanwendung bricht beim Abspielen älterer Ereignisströme ab. Erkennbar ist das an Fehlern beim Öffnen alter Schäden, während neue Schäden funktionieren.
  • Nicht löschbare Personendaten: Namen und Adressen stehen unverschlüsselt in den Ereignissen. Verlangt ein Kunde die Löschung seiner Daten, lässt sie sich nicht umsetzen, ohne die Ereignisse zu verändern. Erkennbar ist das, sobald der erste Löschantrag eingeht.
  • Zu allgemeine Ereignisse: Die Schadenanwendung speichert statt fachlicher Ereignisse wie „Reserve erhöht“ nur „Schaden gespeichert“ mit allen Feldern. Der Ereignisstrom enthält dann zwar jeden Stand, aber nicht, was fachlich geschah. Erkennbar ist das, wenn für eine Frage wie die der Versicherungsaufsicht jeder Stand mit dem vorigen verglichen werden muss.
  • Veraltete Lesetabelle: Eine Sachbearbeiterin ändert die Reserve und sieht in der Liste der offenen Schäden kurz danach noch den alten Betrag, weil die Lesetabelle noch nicht aktualisiert ist. Erkennbar ist das an Rückfragen, die Änderung sei nicht gespeichert worden.

Weiterführend

Verwandte Ansätze

  • Event Driven Architecture: verteilt Ereignisse an andere Programme. Event Sourcing nutzt Ereignisse dagegen, um den eigenen Zustand zu speichern.
  • Kappa Architecture: nutzt ebenfalls ein fortlaufendes Ereignisprotokoll als Quelle, um Auswertungen jederzeit neu zu berechnen.
  • Domain Driven Design: hilft, die fachlichen Ereignisse so zu benennen, wie die Fachabteilung sie versteht, etwa „Reserve erhöht“ statt „Datensatz geändert“.

Werkzeuge und Umsetzungen

  • Axon Framework: Java-Rahmenwerk für Event Sourcing und CQRS.
  • Marten: nutzt die Datenbank PostgreSQL als Speicher für Ereignisströme von Anwendungen auf der Plattform .NET.
  • Eventuous: Bibliothek für Event Sourcing in .NET.
  • Apache Pekko Persistence: speichert den Zustand von Programmteilen in Java und Scala als Folge von Ereignissen.

Quellen