Event Sourcing: der Event-Store als Logbuch
Event Sourcing speichert nicht den aktuellen Zustand, sondern die Folge der Events als Wahrheitsquelle: der Event-Store als Logbuch, der Zustand als Faltungsergebnis. Nutzen, ehrliche Kosten und die Abgrenzung zu CQRS.
In meinen Workshops kommt irgendwann die Frage, die alles verschiebt: „Was speichern wir eigentlich – den Zustand oder das, was passiert ist?" Die meisten Systeme, die ich sehe, speichern den Zustand. Eine Zeile pro Konto, ein Feld balance, und jede Buchung überschreibt den alten Wert. Was passiert ist, ist danach weg. Wir wissen, dass der Kontostand 300 Euro beträgt, aber nicht, wie er dorthin gekommen ist.
Event Sourcing dreht diese Reihenfolge um. Nicht der aktuelle Zustand ist die Wahrheit, sondern die Folge der Ereignisse, die ihn erzeugt haben. Der Event-Store ist ein Logbuch – ein append-only Log, an das nur angehängt wird und in dem nichts geändert oder gelöscht wird. Der aktuelle Zustand ist dann kein gespeichertes Faktum mehr, sondern ein abgeleitetes Artefakt: Man faltet die Events und bekommt den Zustand heraus.
Ich habe das Thema oft zusammen mit CQRS vorgetragen, und genau da entstehen die hartnäckigsten Missverständnisse. Also fange ich mit dem Kern an und ziehe die Grenzen bewusst scharf.
Der Zustand ist ein Left Fold
Martin Fowler beschreibt Event Sourcing als „Capture all changes to an application state as a sequence of events." Jede Zustandsänderung wird als Event festgehalten, und der Anwendungszustand ist damit vollständig aus dem Event-Log ableitbar. Man kann den gecachten Zustand komplett wegwerfen und ihn durch erneutes Abspielen aller Events auf einem leeren Anfangszustand rekonstruieren.
Greg Young hat dafür die didaktisch schönste Formel geprägt: „Current state is a left fold of previous behaviours." Der Zustand ist keine Tabelle, sondern das Ergebnis einer Faltung über die Event-Folge. In Code, den jeder sofort liest:
type Event =
| { type: "AccountOpened"; owner: string }
| { type: "MoneyDeposited"; amount: number }
| { type: "MoneyWithdrawn"; amount: number };
type Account = { owner: string; balance: number };
const apply = (state: Account, event: Event): Account => {
switch (event.type) {
case "AccountOpened":
return { owner: event.owner, balance: 0 };
case "MoneyDeposited":
return { ...state, balance: state.balance + event.amount };
case "MoneyWithdrawn":
return { ...state, balance: state.balance - event.amount };
}
};
const fold = (events: Event[]): Account =>
events.reduce(apply, { owner: "", balance: 0 });
Das ist der ganze Trick, und es ist bewusst wenig. apply nimmt einen Zustand und ein Event und liefert den nächsten Zustand. fold läuft mit reduce über den Stream und ist nichts anderes als ein left fold mit apply als Faltungsfunktion. Der Code hier ist illustrativ – kein lauffähiges Repository, sondern die Idee auf das Wesentliche reduziert.
Ein paar Eigenschaften stecken schon in diesen Zeilen. Die Events sind in der Vergangenheitsform benannt – AccountOpened, MoneyDeposited. Sie sind Fakten, die bereits geschehen sind, und deshalb immutable: Ein Event wird nie geändert und nie gelöscht, es kommt nur ein neues hinzu. Und apply hat keine Seiteneffekte; es ist eine reine Funktion vom alten in den neuen Zustand. Das ist kein Zufall, sondern die Bedingung dafür, dass Replay überhaupt verlässlich funktioniert.
Ich nenne das gern die Definition der Commands, Events und Queries trennen: Ein Command ist eine Absicht, die scheitern darf; ein Event ist das, was daraus tatsächlich geworden ist und nicht mehr scheitern kann. Event Sourcing speichert konsequent die zweite Sorte.
Das Logbuch als Wahrheitsquelle
Schaut man auf den Fluss, ergibt sich ein einfaches Bild. Ein Command trifft ein, das Aggregat entscheidet und erzeugt daraus ein oder mehrere Events, die an das Log angehängt werden. Der Zustand entsteht danach durch Falten.
flowchart LR C[Command] --> A[Aggregate decides] A --> E[New Event] E --> L[(Event Store<br/>append-only)] L --> F[fold / apply] F --> S[Current State]
Wichtig an diesem Bild ist die Richtung. Der Command produziert ein Event, das Log wächst nur nach hinten, und der Zustand hängt am Ende der Kette. Fällt der Zustand weg – Cache verloren, neuer Service, geänderte Berechnung – lässt er sich jederzeit aus dem Log wiederherstellen. Das Log ist maßgeblich, der Zustand ist ableitbar.
Daraus folgen die Vorteile, die Event Sourcing so attraktiv machen – und ich zähle sie bewusst als kleine Liste auf, weil sie zusammengehören:
- Ein vollständiger Audit-Trail entsteht gratis. Nicht als nachgelagertes Protokoll, sondern weil das Event-Log der Zustand ist. Es beschreibt nicht nur, was passiert ist – es ist das, was passiert ist.
- Temporale Abfragen werden möglich. Weil jeder frühere Zustand ein Zwischenergebnis der Faltung ist, kann ich fragen: Wie sah das Konto am 3. Januar aus? Ich falte einfach bis zu dem Event, das an dem Tag geschrieben wurde.
- Projektionen lassen sich neu aufbauen. Ein Read Model, das ich heute nicht habe, kann ich morgen einführen, indem ich die vorhandenen Events erneut durchlaufe. Historie, die bei zustandsbasierter Speicherung längst überschrieben wäre, steht hier vollständig zur Verfügung.
Der dritte Punkt ist der, der Architekten anfängt zu begeistern. Ich muss beim Schreiben nicht vorhersehen, welche Sichten ich später brauche.
Projektionen: Read Models aus dem Stream
Wenn der Zustand ohnehin abgeleitet ist, dann sind es beliebig viele Sichten auf denselben Stream. Genau das sind Projektionen, und sie sind der natürliche Übergang zu den Read Models sind Projektionen: abgeleitete, für Lesezwecke optimierte Modelle, die wegwerfbar und jederzeit neu aufbaubar sind.
type BalanceView = Record<string, number>;
const project = (events: Event[]): BalanceView =>
events.reduce((view, event) => {
switch (event.type) {
case "AccountOpened":
return { ...view, [event.owner]: 0 };
case "MoneyDeposited":
// update the read model per event
return view;
case "MoneyWithdrawn":
return view;
}
}, {} as BalanceView);
Auch project ist ein Fold, nur mit einem anderen Zielmodell. Die eine Faltung ergibt das Aggregat für Entscheidungen, die andere eine flache Übersicht für die Leseseite. Aus demselben Log lassen sich eine Kontoübersicht, ein monatlicher Umsatzbericht und eine Betrugserkennung ableiten – drei Projektionen, drei Faltungen, ein Stream.
flowchart LR L[(Event Store<br/>append-only)] -->|project| A[Read Model A<br/>balances] L -->|project| B[Read Model B<br/>monthly report] L -->|project| C[Read Model C<br/>fraud signals]
Zwei Dinge muss man hier ehrlich benennen. Erstens sind diese Read Models nur eventual consistent – sie hinken dem Write-Log hinterher, weil die Projektion nach dem Anhängen läuft. Wer sofortige Konsistenz zwischen Schreiben und Lesen braucht, bekommt sie hier nicht geschenkt. Zweitens dürfen Projektionen keine externen Seiteneffekte auslösen, und das führt direkt zum ersten ernsten Fallstrick.
Replay hat keine Unschuld
Fowler weist auf ein Detail hin, das in Demos nie auffällt und in Produktion teuer wird: Externe Systeme unterscheiden nicht zwischen echtem Verarbeiten und Replay. Wenn ich ein Read Model neu aufbaue und dabei die Events noch einmal durchlaufe, sieht ein Gateway, das beim ersten Mal eine E-Mail verschickt oder eine Zahlung ausgelöst hat, keinen Unterschied. Es würde die E-Mail erneut senden.
Deshalb gilt die Regel: apply und project sind reine Faltungen ohne Seiteneffekte. Alles, was nach außen wirkt – Mails, Zahlungen, Benachrichtigungen –, gehört nicht in die Faltung, sondern hinter Gateways, die einen Replay-Modus kennen und ihre Effekte dann unterdrücken. Wer das nicht trennt, baut sich beim ersten Rebuild eine Katastrophe.
Snapshots: der gemerkte Fold
Bleibt die Frage nach der Performance. Ein Konto mit zwanzig Buchungen faltet sich in Mikrosekunden. Ein Aggregat mit zweihunderttausend Events tut das nicht mehr bei jedem Zugriff. Die Antwort heißt Snapshot, und Greg Young hat sie so präzise definiert, dass ich sie fast wörtlich zitiere: „A snapshot is a memoization of your left fold."
Ein Snapshot ist eine gespeicherte Momentaufnahme des Faltungsergebnisses bei Event N. Man faltet dann ab dem Snapshot weiter, statt bei Event 0 anzufangen.
type Snapshot = { version: number; state: Account };
const rebuild = (snapshot: Snapshot, newerEvents: Event[]): Account =>
newerEvents.reduce(apply, snapshot.state);
// snapshot = memorized left fold at version N
Der entscheidende Punkt, den ich im Workshop immer betone: Ein Snapshot ist eine reine Performance-Optimierung, keine neue Wahrheitsquelle. Weil die Events immutable sind, kann ein Snapshot nie ungültig werden – die Vergangenheit, aus der er gefaltet wurde, ändert sich ja nicht. Fällt ein Snapshot weg oder wird die Faltungslogik geändert, wirft man ihn weg und faltet neu von vorn. Das Log bleibt maßgeblich; der Snapshot ist nur ein Cache des Folds.
Die Abgrenzung: Event Sourcing ist nicht CQRS
Jetzt zu dem Missverständnis, das ich am häufigsten aufräumen muss. Event Sourcing und CQRS werden fast immer zusammen genannt, und viele halten sie für dasselbe. Sie sind es nicht.
CQRS ist eine fachliche Grenze – die Trennung von Schreibmodell und Lesemodell. Event Sourcing ist eine Persistenzstrategie – Events statt Zustand als das, was gespeichert wird. Das sind zwei verschiedene Entscheidungen auf zwei verschiedenen Achsen. Man kann CQRS ohne Event Sourcing betreiben: getrennte Schreib- und Leseseite, beide klassisch mit Zustandstabellen. Und man kann Event Sourcing ohne strenges CQRS betreiben, auch wenn die Read-Model-Projektionen die Trennung dann fast von selbst nahelegen.
Sie werden oft kombiniert, weil Event Sourcing sehr natürlich Read-Model-Projektionen erzeugt und CQRS genau dafür den passenden Rahmen bietet. Aber keins erzwingt das andere. Wer das trennt, trifft beide Entscheidungen bewusst statt eine im Schlepptau der anderen.
Und noch eine Abgrenzung, die in verteilten Systemen wichtig wird: Event Sourcing ist nicht Event-Driven Architecture. Event Sourcing ist Speicherung und Wahrheitsquelle innerhalb eines Aggregats. Pub/Sub-Messaging zwischen Diensten ist Integration. Ein Domain Event, das ich als Integrationsnachricht über einen Bus schicke, ist nicht automatisch ein gespeichertes Event-Sourcing-Event. Diese Unterscheidung betone ich in meinen Workshops immer wieder, weil das Vermischen beider Ebenen regelmäßig zu Systemen führt, die niemand mehr durchschaut.
Die ehrlichen Kosten
So attraktiv das Logbuch klingt – Event Sourcing ist teuer, und ich verkaufe es nie ohne die Rechnung. Dem Nutzen steht ein realer Preis gegenüber, und beide Seiten gehören nebeneinander betrachtet.
Der Nutzen ist ein vollständiger Audit-Trail, sind temporale Abfragen, nachträglich einführbare Projektionen und eine gute fachliche Semantik durch absichtsvolle Events. Die Kosten sind die Event-Schema-Versionierung über die gesamte Lebensdauer, Replay-Performance samt Snapshot-Verwaltung, die Eventual Consistency der Read Models und eine insgesamt höhere Betriebskomplexität.
Der größte Kostenblock ist die Versionierung. Events leben ewig, und ein Replay muss auch die alten Event-Versionen von vor drei Jahren noch lesen können. Ändert sich das Schema – ein Feld kommt hinzu, eine Bedeutung verschiebt sich –, brauche ich eine Strategie zum Upcasting: alte Events beim Lesen in die aktuelle Form überführen. Wer das unterschätzt, merkt beim ersten großen Rebuild, dass sein Log Events in fünf Generationen enthält, die alle noch gelesen werden wollen. Diesen Aufwand plant man von Anfang an ein oder man plant ihn schmerzhaft nach.
Dazu kommt Eventual Consistency als dauerhafter Begleiter, nicht als Bug. Und Snapshots sind kein Gratiswerkzeug, sondern zusätzliche Infrastruktur, die man verwalten, invalidieren und beim Ändern der Faltungslogik neu erzeugen muss.
Zwei Anti-Pattern, die den Nutzen zerstören
Zwei Fehler sehe ich so oft, dass sie einen eigenen Absatz verdienen.
Der erste ist „CRUD in Verkleidung". Wenn die Events EntityUpdated oder generische FieldChanged heißen, hat man Event Sourcing mechanisch eingebaut, aber die eigentliche Absicht weggeworfen. Ein MoneyWithdrawn erzählt, was fachlich geschah; ein BalanceFieldSetTo erzählt nur, dass irgendein Setter lief. Der ganze Wert – Intent, Audit, Semantik – steckt in der fachlichen Benennung. Generische Setter-Events sind CRUD mit mehr Zeremonie und weniger Nutzen.
Der zweite ist „Event Sourcing überall". Es ist keine Default-Persistenz. Es lohnt sich dort, wo Historie, Audit und temporale Fragen echten fachlichen Wert haben – Buchhaltung, Handel, alles mit regulatorischer Nachvollziehbarkeit. Für einen Stammdaten-CRUD-Dienst, der nur den letzten Stand braucht, ist es teurer Overhead. Die Kunst liegt darin, die Grenze zu ziehen, an der der Kontext das Logbuch wirklich rechtfertigt, und sie nicht aus Begeisterung über das ganze System zu verschieben.
Fazit
Event Sourcing verschiebt eine einzige Annahme: Nicht der Zustand ist die Wahrheit, sondern die Folge der Events, die ihn erzeugt haben. Der Event-Store wird zum Logbuch – append-only, immutable, maßgeblich –, und der Zustand fällt als Left Fold über diesen Stream heraus. Daraus wachsen Audit-Trail, temporale Abfragen und beliebig viele neu aufbaubare Projektionen, fast wie von selbst.
Der Preis dafür ist real: Event-Versionierung über Jahre, Replay-Performance mit Snapshots als gemerktem Fold, Eventual Consistency und die Disziplin, Seiteneffekte aus den Faltungen herauszuhalten. Und die wichtigste gedankliche Trennung: Event Sourcing ist eine Persistenzstrategie, CQRS eine fachliche Grenze – oft gemeinsam, nie voneinander erzwungen.
Wer das nüchtern gegeneinander abwägt, setzt Event Sourcing dort ein, wo die Historie den Aufwand zurückzahlt, und speichert sonst weiter guten alten Zustand. Genau diese Entscheidung – bewusst, nicht aus Mode – ist der eigentliche Gewinn.
Weiterführende Quellen
- Martin Fowler, Event Sourcing: https://martinfowler.com/eaaDev/EventSourcing.html
- Martin Fowler, Retroactive Event (zu Korrekturen durch neue Events): https://martinfowler.com/eaaDev/RetroactiveEvent.html
- Greg Young, Vortrag CQRS and Event Sourcing, Code on the Beach 2014 (Left Fold, Snapshots als „memorized fold")
- Greg Young, Functional Domain Models and Event Sourcing (2012)
- Vaughn Vernon, Implementing Domain-Driven Design (2013) – Kapitel zu Event Sourcing und CQRS
- Mike Bild, Vortragsmaterial CQRS + Event Sourcing (SlideShare) – Autoren-Material, aus dem dieser Artikel schöpft
Kommentare