Event Sourcing verlangt von Ihnen, auf das Überschreiben Ihrer Daten zu verzichten. In einer herkömmlichen CRUD-Anwendung bedeutet das Aktualisieren einer Versandadresse eines Benutzers, die entsprechende Zeile zu finden, den Wert zu ändern und den vorherigen Zustand zu verwerfen. Event Sourcing geht einen anderen Weg. Es speichert jede Änderung als unveränderliche Tatsache: Ein Benutzer hat ein Konto erstellt, seine Adresse aktualisiert oder seine E-Mail verifiziert. Der aktuelle Zustand des Systems wird nicht direkt gespeichert. Er wird berechnet, indem diese Ereignisse in der richtigen Reihenfolge wiederholt werden (Replay).
Dieses Muster löst reale Probleme. Audit-Trails werden zu einem kostenlosen Nebenprodukt. Sie können den Zustand einer Bestellung zu jedem beliebigen Zeitpunkt in der Vergangenheit rekonstruieren. Sie können Fehler beheben (Debuggen), indem Sie exakt das wiederholen, was passiert ist. Der Nachteil ist die Komplexität. Anstatt einfacher Zeilen verwalten Sie nun Streams von Fakten, Read Models und Eventual Consistency.
PostgreSQL kann als Ihr Event Store dienen. Die meisten Teams nutzen es bereits. Es bietet ACID-Transaktionen, JSONB für flexible Payloads und bewährte Backup-Tools. Sie müssen am ersten Tag kein Kafka, Cassandra oder eine spezialisierte Event-Store-Datenbank einführen. Eine Standard-Postgres-Instanz bietet Ihnen die transaktionalen Garantien und Audit-Trails, die Event Sourcing erfordert, ohne Ihren Infrastruktur-Footprint zu vergrößern.
Die Struktur eines Postgres Event Stores
Das Schema kann fast schon peinlich einfach sein. Mindestens benötigen Sie eine Tabelle, die Ereignisse anhängt (Append) und sie niemals an Ort und Stelle aktualisiert. Ein praktisches Design sieht so aus:
idals bigserial oder UUID, dient als globale Sortierung.stream_id, um zusammengehörige Ereignisse zu gruppieren, wie etwa alle Änderungen für einen einzelnen Benutzer oder eine Bestellung.event_typeals Plain Text:UserEmailChanged,PaymentReceived,InventoryAdjusted.payloadals JSONB, das die spezifischen Daten für dieses Ereignis enthält.occurred_atmit Zeitzonenpräzision.versionpro Stream, um optimistische Nebenläufigkeit (Optimistic Concurrency) zu erzwingen.
Sie erzwingen die Unveränderlichkeitsregel im Anwendungscode oder durch eine Datenbankbeschränkung (Constraint). Ein eindeutiger Index auf (stream_id, version) verhindert, dass zwei Schreibvorgänge dieselbe Sequenznummer anhängen. Wenn ein Befehl eingeht, lesen Sie die aktuelle Version für diesen Stream, erhöhen sie und fügen das neue Ereignis innerhalb einer Transaktion ein. Wenn ein anderer Prozess schneller war, schlägt die Unique-Constraint fehl, und Sie wiederholen den Versuch oder lehnen den Befehl ab.
Betrachten wir ein konkretes Beispiel. Sie betreiben ein Inventarsystem. Anstatt einer einzelnen inventory-Zeile mit einer quantity-Spalte hängen Sie Ereignisse an eine inventory_events-Tabelle an. ItemReceived fügt zehn Einheiten hinzu. ItemReserved entfernt zwei. ItemShipped entfernt drei. Um den aktuellen Bestand für SKU-42 zu ermitteln, summieren Sie die relevanten Event-Payloads. Um den Bestand vor drei Tagen zu kennen, summieren Sie nur bis zu diesem Zeitstempel. Wenn ein Fehler in Ihrer Versandlogik am letzten Dienstag zu einem Problem führte, spielen Sie die Ereignisse mit korrigiertem Code erneut ab, um den wahren Zustand zu erhalten. Das können Sie mit einem einfachen UPDATE-Statement nicht tun.
Prinzipien, die Sie vor Problemen bewahren
Der Aufbau auf Postgres entbindet Sie nicht von der Notwendigkeit der Disziplin. Die folgenden Prinzipien gelten direkt für Event-Sourcing-Systeme.
Halten Sie es einfach. Komplexität tötet die Zuverlässigkeit. Widerstehen Sie dem Drang, ein generisches Event-Framework zu bauen, bevor Sie nicht mindestens einen funktionierenden Ablauf implementiert haben. Eine einzelne Tabelle, eine Repository-Funktion zum Anhängen von Ereignissen und ein Projection Worker zum Aufbau von Read Models reichen aus, um den Wert zu beweisen. Fügen Sie erst dann Tools hinzu, wenn ein konkretes Problem auftritt.
Fangen Sie klein an. Schreiben Sie nicht Ihren gesamten Monolithen um. Wählen Sie einen Bounded Context, in dem der Audit-Trail den Overhead rechtfertigt. Ein Abrechnungsbuchhaltungssystem, eine Workflow-Engine oder ein Reservierungssystem für Bestände sind gute Kandidaten. Bauen Sie diese eine Pipeline von Ende zu Ende auf. Lassen Sie sie in der Produktion laufen. Entscheiden Sie erst dann, ob Sie expandieren möchten.
Definieren Sie zuerst den Erfolg. Event Sourcing ist keine Standardarchitektur, sondern eine Lösung für spezifische Anforderungen. Wenn Ihre Anforderung lediglich darin besteht, den neuesten Zustand zu verfolgen, ist CRUD schneller und kostengünstiger. Wenn Sie temporale Abfragen, strikte Revisionssicherheit oder die Möglichkeit benötigen, Read Models auf Abruf neu aufzubauen, dann ist Event Sourcing sinnvoll. Wissen Sie, welches Problem Sie lösen, bevor Sie sich festlegen.
Messen Sie, bevor Sie optimieren. Modernes PostgreSQL auf bescheidener Hardware kann mit einer einfachen Append-only-Tabelle Tausende von Ereignissen pro Sekunde aufnehmen. Führen Sie kein Sharding Ihres Event Stores durch oder führen Sie komplexe Partitionierungsschemata ein, bevor Ihr Monitoring beweist, dass Sie einfachere Lösungen ausgeschöpft haben. Indizieren Sie die Felder, die Sie abfragen. Optimieren Sie autovacuum für Append-only-Workloads. Messen Sie dann erneut.
Testen Sie alles. Führen Sie Unit-Tests für Ihre Event-Handler durch. Testen Sie den Append-Pfad mittels Integrationstests. Am wichtigsten ist es, Fehlerszenarien zu testen. Was passiert, wenn zwei Knoten gleichzeitig an denselben Stream anhängen? Was passiert, wenn ein Projection-Worker mitten in einem Batch abstürzt? Schreiben Sie Tests, die Ihre optimistische Nebenläufigkeit (Optimistic Concurrency) und Ihre At-Least-Once-Delivery-Garantien verifizieren.
Überwachung in der Produktion. Die Events-Tabelle wird wachsen. Im Gegensatz zu einem normalisierten Schema, bei dem Updates die Zeilenanzahl konstant halten, ist Event Sourcing absichtlich additiv. Überwachen Sie die Tabellengröße, den Disk-I/O und den Lag zwischen Ihrem Write-Model und Ihren Read-Model-Projections. Richten Sie Alarme für den Projection-Lag ein, bevor Ihre Nutzer veraltete Daten bemerken.
