- Das relationale CRUD-Modell zeigt die Speicherstruktur gut, überschreibt aber leicht den Geschäftsprozess, sodass es schwer ist nachzuvollziehen, was im System tatsächlich passiert ist
- Event Sourcing hinterlässt nach jeder Operation unveränderliche Events als Event Stream und liest diese Liste später für Entscheidungen, um den aktuellen Zustand zu bestimmen
- Die Modellierung beginnt damit, zuerst Events zu finden, und verbindet dann Befehle (command) als Handlungsabsicht der Nutzer mit den Geschäftsregeln, um den Prozess zu verstehen
- Um Event-Kandidaten in bestehenden relationalen Daten zu finden, betrachtet man Statusspalten, Datumsspalten, Nullable-Eigenschaften und 1:N-Beziehungen; es ist jedoch riskant anzunehmen, dass sich allein aus Statuswerten eine vollständige Historie rekonstruieren lässt
- Wenn nur noch Daten mit Endzuständen migriert werden, ist es praxisnäher, statt vergangene Events künstlich zu rekonstruieren, mit einem expliziten Import-Event wie Order Imported zu beginnen und dies in einer sicheren Umgebung wiederholt zu validieren
Vom CRUD-Datenmodell zum eventzentrierten Modell
- Relationale Datenmodelle zeigen, welche Daten gespeichert werden, machen aber nur schwer verständlich, was im System passiert ist und wie Prozesse miteinander interagieren
- Der bestehende CRUD-Ansatz kann wichtige Geschäftsinformationen verlieren, weil Daten überschrieben werden
- Event Sourcing priorisiert Informationsqualität statt Speichergröße und speichert die nach jeder Operation entstandenen Fakten als Events
Grundmodell von Event Sourcing
- Ein Event ist ein Fakt über etwas, das bereits passiert ist, und ist nach dem Speichern unveränderlich
- Ein Event Stream ist eine geordnete Liste aller Vorgänge, die zu einem Datensatz passiert sind
- Vergangene Events können nicht geändert werden, aber am Ende kann ein neues Event hinzugefügt werden, um frühere Fehler zu korrigieren
- Bei Entscheidungen wird die Event-Liste gelesen und geprüft, um den aktuellen Zustand und die nächste Aktion zu bestimmen
Reihenfolge der Prozessmodellierung
- Die Modellierung beginnt mit der Entdeckung von Events
- Danach werden Befehle (command) identifiziert, um die beabsichtigte Handlung zu definieren
- Zum Schluss werden die Geschäftsregeln geordnet
- Events werden zur zentralen Achse, über die technische und fachliche Beteiligte den Prozess gemeinsam verstehen
- Mit EventStorming von Alberto Brandolini lassen sich Events, Befehle und Regeln gemeinsam betrachten, um den Prozess zu erfassen
Event-Kandidaten in bestehenden relationalen Daten finden
-
1. Statusspalten untersuchen
- Die Werte einer
status-Spalte können Phasen des Datenlebenszyklus widerspiegeln - Wenn eine Bestellung Zustände wie initiated, shipped oder paid hat, können diese jeweils Kandidaten für die Events Order Initiated, Order Shipped und Order Paid sein
- Statuswerte können jedoch eine abgeflachte Interpretation des Geschäftsprozesses sein; man darf sie nicht als vollständig annehmen
- Event-Namen sollten nicht nach CRUD-Aktionen wie Order Created, Order Updated oder Order Deleted vergeben werden
- State Obsession wird als zu vermeidender Ansatz genannt
- Die Werte einer
-
2. Datumsspalten prüfen
- Datumsspalten können wichtige Zeitpunkte des Geschehens im Prozesslebenszyklus sichtbar machen
- CreatedDate und ModifiedDate liefern wenig Information, aber ShipmentDate, DeliveryDate und OrderPlacementDate sind bessere Hinweise
- Beispiele:
- ShipmentDate kann ein Hinweis darauf sein, das Event Order Shipped einzuführen
- OrderPlacementDate deutet an, dass Order Placed ein besserer Name als Order Initiated sein könnte
- DeliveryDate zeigt, dass möglicherweise ein Event Order Delivered benötigt wird
- Diese Hinweise sollten mit Domänenexpert:innen abgeglichen werden, damit sie zum tatsächlichen Geschäftsprozess passen
-
3. Nullable-Eigenschaften von Spalten analysieren
- Non-nullable-Spalten enthalten Daten, die immer vorhanden sein müssen
- Nullable-Spalten können Werte enthalten, die später durch andere Operationen geliefert werden oder optional sind
- Wenn in einem Ordering Process bestimmte Spalten Pflichtfelder sind, sollten diese Daten auch im ersten Event Order Initiated enthalten sein
- Ein einzelner Event-Typ ist nicht zwingend immer der Startpunkt eines Streams; es kann mehrere Start-Events geben
-
4. Tabellen mit vielen 1:N-Beziehungen finden
- Um Stream-Grenzen zu finden, kann man zuerst Tabellen mit vielen 1:N-Beziehungen betrachten
- Tabellen mit vielen „one“-Beziehungen sind Kandidaten für einen Stream-Typ
- Außerdem sollte logisch beurteilt werden, ob Daten unabhängig voneinander existieren können
- shipment kann ein separater Prozess neben order sein
- order line kann nur schwer ohne order existieren
- Bei der Diskussion über Grenzen lassen sich oft weitere Events entdecken und das Prozessverständnis vertiefen
Bei der Migration keine falschen Events erzeugen
- Relationale Daten sind ein abgeflachter Endzustand; wenn man allein daraus versucht, die tatsächlichen detaillierten historischen Events zurückzurechnen, scheitert das oft oder bleibt ungenau
- Statt kleinteilige historische Events zu erzwingen, ist es besser, explizit ein Event wie Order Imported bereitzustellen, das den vollständigen aktuellen Zustand und den Interpretationscode enthält
- Ein Import-Event zeigt klar, auf welche Weise die Daten übernommen wurden, und kann für Fehlersuche und Diagnose wichtig sein
Mit einem Prototyp validieren
- Eine Migration sollte in einer sicheren Umgebung als Prototyp ausprobiert werden, um zu prüfen, wie sich das Modell tatsächlich verhält
- Die Ergebnisse sollten mit den Erwartungen verglichen und iterativ angepasst werden
- Es braucht einen Ansatz, der nicht übereilt vorgeht, keine bestehenden Informationen verliert und das Modell auf Basis dieser Informationen später verbessert
- Die allgemeine Strategie zur Migration relationaler Daten in dokumentbasierte Modelle wird auch in General strategy for migrating relational data to document-based behandelt
1 Kommentare
Meinungen auf Hacker News
2c: Wenn ihr PostgreSQL auch in anderen Teilen der App braucht, ist es sinnvoll, die Event-Daten ebenfalls in PostgreSQL + FOSS-Reporting-Tools (Apache Superset, Metabase usw.) zu speichern und damit grob bis 2 TB durchzuhalten.
Danach könnt ihr entscheiden, ob die gesamten 2 TB online bleiben müssen oder ob Zusammenfassungen auf Tages-/Stundenbasis reichen. Wenn Letzteres der Fall ist, reicht es völlig, weiter bei PostgreSQL zu bleiben[1].
Ein Kunde verarbeitet mehr als 10 TB, 1.500 Events pro Sekunde und 600 Byte pro Datensatz (80 GB pro Tag vor der Indizierung), hält nur Detaildaten für 2 Tage online, fasst den Rest zusammen und verschiebt die Details nach S3, wo sie weiterhin per Athena SQL abgefragt werden[2].
Die Gesamtkosten einschließlich Reporting-Portal für Kunden liegen unter 2.000 Dollar, und auf AWS RDS mit Multi-AZ-Auto-Failover (db.m7g.2xlarge) werden sowohl Inserts als auch Reporting-Queries verarbeitet; die Last liegt unter 2 %. Weil das Business-Team Diagramme und Graphen selbst erstellt, verbringt ein Engineer weniger als 5 Stunden pro Monat mit Wartung.
Bei proprietären Tools sind ein paar Diagramme zwar „out of the box“ dabei, aber mit pgsql liegen die Daten an einem Ort, es gibt nur ein System zu lernen, nur ein System online zu halten/zu replizieren/zu sichern/wiederherzustellen, nur ein System abzusichern/zu skalieren, nur einen Vendor zu verwalten, und es gibt Millionen Engineers, die dieses System kennen.
In Systemen wie Preset oder Metabase dauert es eine Stunde, 12 Diagramme zu erstellen, und auch nichttechnische Mitarbeitende können das tun.
Zur Einordnung: Ich bin zwar voreingenommen, habe aber über mehr als 20 Jahre Datenbanken und Reporting-Systeme kommen und gehen sehen, und das gute alte PostgreSQL wird jedes Jahr besser.
https://instances.vantage.sh/aws/rds/db.m7g.2xlarge?region=u...
[1] Wenn es wirklich nötig ist, gibt es auch PostgreSQL-kompatible Systeme für zusätzliche Skalierung. Aurora kann etwa 3–5× skalieren, TimescaleDB etwa 10×, CitusDB etwa 10×+. Das geht jeweils mit dem Preis einher, etwas weniger standardkonform zu werden; daher würde ich es nicht empfehlen, bevor es tatsächlich nötig ist.
[2] Das Kunden-Reporting-Dashboard braucht Antwortzeiten unter 1 Sekunde; PostgreSQL liefert das durch Abfragen indizierter Zusammenfassungstabellen. Athena antwortet durch parallele Scans in etwa 1–2 Sekunden.
Man behält Snapshots der Daten vor dem Speichern, hat Skripte zum Identifizieren und Sammeln einer bestimmten Event-Reihenfolge und lässt dann einen Menschen prüfen, um die Auswirkungen neuer Logik massenhaft rückwirkend anzuwenden.
Tools wie https://django-simple-history.readthedocs.io/en/latest/ sind eine halbwegs zuverlässige und einfache Lösung zum Erstellen von Audit-Tabellen; wenn man sogar direkten Datenbankzugriff auditieren muss, kann man zusätzlich Postgres-Trigger verwenden.
In der Theorie mag ich Event Sourcing, aber in der Praxis bringt es zu viel Boilerplate mit sich, um neue CRUD-Flows hinzuzufügen oder Eingriffe und Hotfixes, wie sie Early- bis Mid-Stage-Startups in unerwarteten Situationen häufig brauchen, schnell und zuverlässig auszurollen.
Wenn man nicht gerade etwas wie Payment-Processing-Rails implementiert, ist Event Sourcing möglicherweise nicht die richtige Wahl.
Auch unter https://news.ycombinator.com/item?id=17817375 (2018) gibt es eine gute Diskussion über die Nachteile von Event Sourcing.
Das einzige Problem bei PostgreSQL ist, dass es auf der Insert-Seite interessante Skalierungsprobleme gibt. Üblicherweise empfiehlt man, eine Queue zwischen Event-Quelle und DB zu setzen.
{id:uuid,created_at:timestamptz,data:jsonb}hat.Besonders wenn die Event-Strukturen vielfältig sind und sich Event-Definitionen weiterentwickeln, ist es schwierig, die Funktionen von JSONB-Indizes richtig auszunutzen.
Vermutlich muss ich mich in diese Doku noch besser einarbeiten: https://www.postgresql.org/docs/current/datatype-json.html#J...
Früher hat unser Team einmal Event Sourcing ernsthaft in Betracht gezogen, aber für mich wirkte es wie eine Lösung auf der Suche nach einem Problem.
Es hätte bei uns vielleicht funktionieren können, aber die Vorteile waren nicht sofort klar, und die Risiken und Lernschleifen, die wir uns mit der Einführung eines neuen Ansatzes eingehandelt hätten, schienen weder für das Projekt noch für das Unternehmen das Beste zu sein. Also haben wir es letztlich verworfen.
Vielleicht war das eine Entscheidung, bei der uns ein Werkzeug und eine Lernchance entgangen sind, aber ich bereue nicht, nicht in dieses Rabbit Hole gegangen zu sein, zumal uns kein Fuchs im Nacken saß.
Genau das ist das „Problem“, das diese Lösung löst.
In den meisten Fällen reicht es aber, eine normale Datenbank zu verwenden und frühere Änderungsverläufe in Hilfstabellen zu speichern. Dann verhält sich die Hauptdatenbank wie eine Art materialisierte View.
Ich habe keine großen Beschwerden und würde auch nicht sagen, dass es die falsche Wahl war, aber bei der Art, wie Änderungen am Datenmodell gehandhabt werden, entstehen Probleme.
Es wirkt so, als hätten die meisten Arten der Datenspeicherung nicht mit der heutigen Art, Software zu bauen, Schritt gehalten; Dinge wie Events und Queues sind das Ergebnis davon, benötigte Funktionen auf bestehende Systeme aufzusetzen.
Viele Datenbeziehungen finden heute zwischen mehreren Services statt, also außerhalb der Datenbank. So sieht die moderne IT-Landschaft in vielen Organisationen nun einmal aus.
Es gibt interne Stammdaten, die mehrere Business-Teams unterstützen, und sie interagieren mit über 300 IT-Systemen und Anwendungen, um Abläufe zu vereinfachen.
Mit Microservices ist es einfacher, Business-Logik und Datenmodelle sauber zu halten, aber dafür muss man Events, Queues, Datenzustände und abhängige Stores verwalten, und das ist inzwischen viel zu komplex.
Ich mag SQL, aber ehrlich gesagt würden die Systeme, die wir heute bauen, vermutlich fast vollständig in SQLite passen.
Was in solchen Diskussionen fehlt, ist die Frage, wann eine eventgetriebene Architektur angemessen ist.
Kurz gesagt: Wenn ein Kunde etwas getan hat und eine Antwort erwartet, ist das nicht eventgetrieben, sondern einfach Request/Response.
Eventgetrieben ist es, wenn etwas außerhalb des normalen Bandes passiert. Zum Beispiel, wenn man Code zu GitHub pusht und dadurch ein Build ausgelöst wird.
In diesem Beispiel ist das Neuladen der Seite, um den aktualisierten Code zu sehen, Request/Response, während der in die Queue gestellte CI-Build eventgetrieben ist.
Hoffe, das hilft.
Auch mit Event Sourcing oder eventgetriebenen Ansätzen kann man Request-Response-, Inline-, blockierende und zyklische Abläufe bauen.
Umgekehrt kann man auch ohne Event Sourcing oder eventgetriebene Architektur mit Workern, Queues, Actors, Multithreading und ähnlichen Ansätzen gute Asynchronität umsetzen.
Domain Events zu modellieren ist nützlich, um mit Fachexperten das zu lösende Problem zu beschreiben, und es kann sinnvoll sein, sie bei der Planung der Lösung in der Dokumentation festzuhalten.
Wenn man tatsächlich ein System implementieren will, das einen Audit Trail für eine langlebige Zustandsmaschine bereitstellt, ist es wahrscheinlich besser, etwas wie Temporal.io oder Durable Functions zu verwenden.
Solche Tools nutzen intern Event Sourcing für die Persistenz und bieten ein Programmiermodell, das Code zur Orchestrierung von Funktionen (Workflows) und Code zur Interaktion mit der realen Welt (Activities) mit unterschiedlichen Einschränkungen versieht und einen dazu zwingt, über Deduplizierung und Idempotenz nachzudenken.
Ich würde gern Vorschläge hören, wie man dieses Problem überwindet.
Das Konzept ist interessant, aber der Artikel erklärt nicht gut, wie es funktioniert
Mich würde interessieren, wie man den aktuellen Zustand aus einem Event-Stream effizient rekonstruiert und wie man einen Event-Stream in einer Datenbank modelliert
https://www.youtube.com/watch?v=gG6DGmYKk4I
https://www.youtube.com/watch?v=jnDchr5eabI
https://www.youtube.com/watch?v=ArcypYS5XBQ
https://www.youtube.com/watch?v=uODSwR2CIV4
Außerdem pflegt er Beispiele auf GitHub
https://github.com/oskardudycz/EventSourcing.NetCore
https://github.com/oskardudycz/EventSourcing.NodeJS
https://github.com/oskardudycz/EventSourcing.JVM
Erstens: Man verwendet eine Datenbank, die für diesen Zweck entworfen wurde. Beispiele sind Google BigQuery, Amazon Redshift, ClickHouse usw.
Alle aktuellen Daten sind im Grunde eine Art Aggregation. Anders gesagt: wie eine group-by-Abfrage auf einer Event-Datenbank
Wenn Events vorhanden sind, kann man den aktuellen oder einen früheren Zustand technisch per Aggregationsabfrage wiederherstellen; das ergibt also Sinn
Zweitens: Man benennt den relationalen Speicher um und nennt ihn eine Cache-Schicht neben dem Event-System
Funktional ist es dasselbe, löst aber bei Leuten, die sich darauf versteifen, alles eventgetrieben machen zu müssen, keine Warnlampen aus
Die im Artikel beschriebene Architektur gibt es tatsächlich. Sie ist allerdings extrem komplex, und Dienste, die sie nutzen, machen normalerweise sehr gezielte Dinge. Man denke an Google Analytics, Datadog oder Splunk
Man kann je nach unterschiedlichen Anforderungen unterschiedliche Zustände in unterschiedlichen Systemen erzeugen
Wenn man ein Einkaufssystem baut und Käufe sowie Kunden hat, kann ein Dienst die Events lesen und relationale Tabellen für Finanzzwecke erstellen
Ein anderer Dienst kann die Events lesen und einen Key-Value-Store für Kundendaten aufbauen, und ein dritter Dienst kann einen OpenSearch-Dienst für die Produktsuche betreiben
Ein Event-Stream ist eine Liste. Wenn man etwas Zweckmäßiges wie Kafka verwendet, werden daraus mehrere Listen, also Topics, Partitionen usw.
Aber auch das lässt sich innerhalb eines relationalen Modells lösen
Das ist der Unterschied zwischen Top-down vs. Bottom-up, oder zwischen maßgeschneidert und allgemein einsetzbar
Top-down bedeutet, von der Business-Domain auszugehen und die Implementierung auf verfügbare Technologien, Tools und Anbieter abzubilden
Bottom-up bedeutet, von verfügbaren Technologien, Tools und Anbietern auszugehen und daraus eine funktionierende Lösung zusammenzusetzen
Zum Maßgeschneiderten gehören DDD, CQRS/ES, Sagas, TBUI (task-basierte/-getriebene UI), GraphQL, algebraische Datentypen usw.
Zum Allgemeinen gehören RDBMS, CRUD, REST, ACID-Transaktionen, CDC, generische Admin-UIs, No-Code/Low-Code, eingeschränkte/allgemeine Typen usw.
Ich werde einfach weiter die gute alte relationale Datenhaltung verwenden
Ich stimme eventbasierten Architekturen zu, aber dieser Artikel scheint Mühe zu haben, den Punkt zu vermitteln
Ich würde mich auf den Unterschied zwischen Datenbeziehungen und geschäftlichen Handlungen konzentrieren
Sobald man beginnt, in Handlungen und Geschäftsaktivitäten zu denken, wird die Bewegung weg von operativen relationalen Datenspeichern viel deutlicher
Event Sourcing hat viele gute Eigenschaften und ist deshalb interessant
Aber braucht man nicht trotzdem Beziehungen? Wenn ja, wie werden diese Beziehungen implementiert?
Wenn die Antwort lautet: „Sie stecken alle implizit im Code der Anwendungsschicht“, fällt es mir schwer, das zu akzeptieren
Man braucht trotzdem die Möglichkeit, Beziehungen abzufragen, relationale Views aktuell zu halten oder etwas Ähnliches
Es ist in Ordnung, wenn Beziehungen nicht der Kern des Persistenzmodells sind, aber irgendwo in der Datenschicht müssen sie implementiert sein; dazu sehe ich hier keine Erwähnung
Firestore hat dasselbe Problem. Alle gehen irgendwie mit Beziehungen um, aber am Ende wird daraus nicht skalierbarer Spaghetti-Anwendungscode
Wenn man mit funktionaler Programmierung vertraut ist, ist es im Kern dasselbe wie eine fold-Operation, die einen Event-Stream zu einem Zustand zusammenfaltet
Aus meiner früheren Arbeit mit Event-Sourcing-Systemen: Es hat den Vorteil einer explizit gespeicherten Event-Historie, erhöht aber auch die Komplexität erheblich
Es entstehen Fragen wie: Wie erzeugt man Read Models konkret, wie verwaltet man Modellversionen, legt man Snapshots von Read Models an usw.
Meiner Erfahrung nach war die zusätzliche Komplexität in den meisten Kontexten, in denen dieses Pattern eingesetzt wurde, den Aufwand nicht wert
Was man braucht, ist eine Command Queue. Command Events sind keine Domain Events