Die meisten Anwendungen lesen und schreiben dieselben Tabellen über dasselbe Modell. Das funktioniert, bis die Formen auseinanderlaufen: Schreibvorgänge brauchen strenge Geschäftsregeln für jeweils eine Bestellung, Lesevorgänge wollen Listen, Suchen und Summen über Tausende Bestellungen. CQRS trennt beides, und Event Sourcing ändert, was du überhaupt speicherst.
Die beiden werden oft als Paar präsentiert, sind aber getrennte Entscheidungen. Dieser Artikel erklärt beide, wie sie zusammenpassen, wie das Diagramm eines kombinierten Systems aussieht und welche Kosten viele Teams bereuen lassen, sie zu früh eingeführt zu haben.
CQRS in einem Absatz
Command Query Responsibility Segregation bedeutet, ein Modell zum Ändern von Daten und ein anderes zum Lesen zu verwenden. Commands (PlaceOrder, CancelOrder) gehen an eine Schreibseite, die die Regeln durchsetzt. Queries gehen an eine Leseseite, die auf die Ansichten zugeschnitten ist, die sie brauchen. Martin Fowlers Artikel zu CQRS, der das Pattern Greg Young zuschreibt, enthält auch die Warnung, die man wiederholen sollte: Für die meisten Systeme bringt CQRS riskante Komplexität. Setz es dort ein, wo Lesen und Schreiben wirklich auseinanderlaufen, nicht in der ganzen Anwendung.
CQRS erfordert keine zwei Datenbanken. Ein Schreibmodell und ein paar SQL-Views über dieselben Tabellen sind schon CQRS. Getrennte Speicher kommen später, wenn Leselast oder Leseformen sie verlangen.
Event Sourcing in einem Absatz
Event Sourcing speichert die Änderungen statt des aktuellen Zustands. Eine Bestellung ist keine Zeile mit einer Status-Spalte, sondern ein Stream von Events: OrderPlaced, PaymentAuthorized, OrderShipped. Der aktuelle Zustand ist das, was beim Abspielen dieser Events herauskommt. Fowlers Artikel zu Event Sourcing beschreibt die Idee im Detail. Du bekommst eine vollständige Audit-Historie gratis und kannst jederzeit neue Sichten auf die Vergangenheit bauen, weil nichts überschrieben wurde.
Wie beides zusammenpasst
Event Sourcing erzwingt CQRS fast. Events abzuspielen ist ein guter Weg, eine einzelne Bestellung vor einer Änderung zu laden, aber ein schrecklicher Weg, um „Zeig mir alle offenen Bestellungen über 100 Dollar“ zu beantworten. Deshalb arbeitet die Schreibseite mit Event-Streams, und die Leseseite hält separate Read Models, die aus diesen Events aktualisiert werden.
Der Ablauf:
- Ein Command kommt auf der Command-Seite an.
- Der Handler lädt das Aggregate (die Bestellung), indem er seinen Stream abspielt, prüft die Geschäftsregeln und entscheidet, welche neuen Events entstehen.
- Er hängt die Events mit der erwarteten Version des Streams an den Event Store an. Hat inzwischen ein anderes Command etwas angehängt, schlägt das Anhängen fehl und das Command wird wiederholt. Das ist optimistische Nebenläufigkeitskontrolle, und sie ersetzt Zeilensperren.
- Projektoren abonnieren den Event-Stream und aktualisieren Read Models: eine Tabelle für die Bestellliste, einen Suchindex, Zähler für ein Dashboard.
- Queries lesen nur aus Read Models.
Der Event Store
Ein Event Store braucht wenige Funktionen, aber genau diese:
- Append-only-Streams, einer pro Aggregate, mit einer Versionsnummer pro Event.
- Bedingtes Anhängen auf die erwartete Version, atomar.
- Einen Stream lesen, vom Anfang oder ab einer Version.
- Subscriptions auf alle neuen Events in Reihenfolge, mit einer Position, die ein Projektor speichern und von der er weitermachen kann.
Spezialisierte Event Stores bieten das, und eine relationale Tabelle mit einem Unique-Constraint auf Stream-ID und Version leistet es ebenfalls, zusammen mit einer Outbox oder einem Change Feed für die Subscriptions.
Read Models und Eventual Consistency
Projektionen laufen, nachdem das Event gespeichert ist, also hinken Read Models um Millisekunden hinterher, bei einem Rückstau auch viel länger. Plane dafür:
- Gib nach einem Command die neue Version zurück und lass den Client warten, bis das Read Model aufgeholt hat, oder zeig das Ergebnis aus der Command-Antwort.
- Gib jedem Projektor seine eigene Position, damit ein langsamer Suchindex die Bestellliste nicht verzögert.
- Mach Projektionen idempotent. Projektoren starten neu und lesen Events erneut; OrderShipped zweimal anzuwenden darf nicht zwei Sendungen zählen.
Der Gewinn: Read Models sind wegwerfbar. Ein neuer Screen braucht eine neue Form? Schreib einen Projektor und spiel die Historie in ihn ein. Ein Bug in einer Projektion? Beheb ihn, verwirf das Read Model und bau es neu auf.
Ein konkretes Beispiel: ein neuer Report
Einen Monat nach dem Launch will die Finanzabteilung den Umsatz pro Produktkategorie und Tag, inklusive Historie. In einem CRUD-System schreibst du eine Migration und ein Backfill-Skript, und die Historie vor der Änderung ist vielleicht schon weg. Mit Event Sourcing schreibst du einen Projektor, der auf OrderPlaced und OrderRefunded hört, setzt ihn auf den Anfang des Event-Logs und lässt ihn abspielen. Hat er aufgeholt, deckt der Report jede Bestellung seit dem Launch ab und hält sich ab dann selbst aktuell.
Das Architekturdiagramm
Diagramminhalt
- User
- Command side
- Event store
- Event stream
- Projectors
- Read model: order list
- Read model: search index
- Read model: daily totals
- Query side
- PlaceOrder
- append
- subscription
- GET /orders
So liest du es:
- Obere Reihe, der Schreibpfad: Das PlaceOrder-Command des Nutzers geht an die Command-Seite, die Events an den Event Store anhängt. Eine Subscription speist den Event-Stream.
- Rechts: Projektoren konsumieren den Stream und halten drei Read Models aktuell, jedes in dem Speicher, der zu seinen Queries passt.
- Unten links, der Lesepfad: Queries gehen an die Query-Seite, die nur Read Models liest und keine Geschäftsregeln enthält.
- Der Markdown-Block zeigt einen Stream mit seinen Versionen, was die Prüfung der erwarteten Version leicht erklärbar macht.
Die Kosten
- Schema-Evolution hört nie auf. Events werden für immer gespeichert, also müssen alte Event-Versionen lesbar bleiben. Plane Upcasting (alte Events beim Lesen in die aktuelle Form umwandeln) vom ersten Tag an ein.
- Daten zu löschen ist schwer. Vorschriften, die Löschung verlangen, kollidieren mit unveränderlichen Logs; halte personenbezogene Daten aus Events heraus oder verschlüssele sie pro Person und lösche den Schlüssel.
- Lange Streams laden langsam. Snapshots des Aggregate-Zustands alle paar hundert Events lösen das.
- Debugging verlagert sich. Bugs zeigen sich als falsche Read Models, und die Behebung ist ein Replay, das Tooling braucht.
- Fähigkeiten im Team. Event Modeling ist eine andere Denkweise. Eine Kerndomäne mit Event Sourcing ist reichlich; der Rest des Systems kann CRUD bleiben.
Was ins Diagramm gehört
- Zwei klar getrennte Pfade für Commands und Queries.
- Der Event Store als Quelle der Wahrheit, die Read Models als abgeleitet gezeichnet.
- Jeder Projektor und sein Read Model, damit Leser sehen, welche Sichten hinterherhinken können.
- Ein Beispiel-Stream mit Versionen, um die Diskussion zu verankern.
Die Mechanik von Events und Consumern behandelt der Artikel zur Event-Driven-Architektur, und das Transactional-Outbox-Pattern erklärt, wie du Events zuverlässig veröffentlichst, wenn du keinen spezialisierten Event Store nutzt. Das Kafka-Architekturdiagramm behandelt den Stream selbst.
In Archboard nachbauen
- Melde dich mit Google oder GitHub an und leg ein Diagramm an.
- Füge aus dem C4-Modell-Pack eine Person und Container-Nodes für die Command-Seite, die Projektoren und die Query-Seite hinzu.
- Füge eine Datenbank für den Event Store und Datenbank-Nodes für die Read Models hinzu, dazu Kafka und Redis aus dem Daten-Pack für den Event-Stream und die Zähler.
- Zeichne den Schreibpfad oben und den Lesepfad darunter; die Projektoren speisen die Read Models von rechts.
- Füge einen Markdown-Block aus dem Basis-Pack mit einem Beispiel-Event-Stream hinzu.
Das Beispiel nutzt 10 Diagramm-Nodes und bleibt damit im Free-Plan.
FAQ
Müssen CQRS und Event Sourcing zusammen verwendet werden? Nein. CQRS funktioniert mit einer normalen Datenbank, und Event Sourcing kann ein einziges Read Model speisen. Event Sourcing ganz ohne CQRS ist selten, weil Queries über abgespielte Streams langsam sind.
Wann sollte ich CQRS nicht verwenden? Wenn Lesen und Schreiben dieselbe Datenform nutzen, was für die meisten CRUD-Screens gilt. Setz es in den Teilen des Systems ein, in denen beides auseinanderläuft.
Wie bleiben Read Models mit dem Event Store konsistent? Mit Verzögerung, also eventual consistent. Projektoren verarbeiten Events in Reihenfolge und speichern ihre Position; nach einem Fehler machen sie dort weiter. Clients müssen eine kurze Verzögerung tolerieren.
Wie ändere ich später die Struktur eines Events? Versioniere Events und wandle alte Versionen beim Lesen um (Upcasting). Bearbeite gespeicherte Events nie direkt.
Was ist ein Snapshot beim Event Sourcing? Eine gespeicherte Kopie des Zustands eines Aggregates bei einer bestimmten Version. Zum Laden liest man dann den Snapshot plus die Events danach statt des ganzen Streams.