Playbook

CQRS (Command Query Responsibility Segregation) in verteilten Systemen: Events, Broker und Projektionen (CQRS In Verteilten Systemen Events Broker Und Projektionen)

Wie funktioniert CQRS in verteilten Systemen? Ein praxisnaher Leitfaden zu Domain Events, Message Brokern, Projektionen, Outbox, Idempotenz und Eventual…

Distributed CQRS event flow from command to projections and read models

Zuerst das Problem, das wir lösen

Eine Bestellung wurde erstellt. Inventory muss es wissen, Benachrichtigungen müssen eine E-Mail senden, Analytics muss einen Bericht vorbereiten und Search muss die neue Bestellung indexieren.

Order Service
  → HTTP-Aufruf für Inventory
  → HTTP-Aufruf für Notifications
  → HTTP-Aufruf für Analytics
  → HTTP-Aufruf für Search

Soll der Order Service jede Komponente per HTTP aufrufen? Jede neue Fähigkeit vergrößert Call Chain, Fehlerfläche und den Druck zum gemeinsamen Deploy. Nein. Der Order Service sollte die eingetretene Geschäfts-Tatsache teilen; wie sie verwendet wird, liegt bei jedem Consumer.

In den ersten beiden Kapiteln haben wir getrennt, wie eine Anfrage durch Command- und Query-Seite läuft. In einem verteilten System entsteht eine neue Frage: Wie erfahren Inventory, Benachrichtigungen, Analytics und Search nach einer Bestellung sicher von der Änderung?

POST /orders → Command Handler → OrderPlaced Event → Outbox → Relay → Message Broker
  ├─ Inventory-Projektion
  ├─ Notification-Consumer
  ├─ Analytics-Projektion
  └─ Search Read Model

Ein Event ist eine unveränderliche Geschäfts-Tatsache; OrderPlaced ist keine Absicht. Ein Broker transportiert es vom Producer zu Consumern. Ein Consumer verarbeitet es für seine Verantwortung. Eine Projektion erstellt ein Read Model für eine bestimmte Frage.

Begriffe bei der ersten Verwendung

📦 Domain Event
Eine unveränderliche Geschäfts-Tatsache, im Präteritum benannt.

📦 Broker
Die Kommunikationsschicht, die eine Nachricht dauerhaft an Consumer verteilt.

📦 Projektion
Ein aus Events erzeugtes Read Model für einen bestimmten Screen oder eine Query.

📦 Idempotenz
Die Eigenschaft, dass ein zweites Verarbeiten desselben Events das Ergebnis nicht verändert.

Ein Command sagt PlaceOrder; er bittet das System zu handeln. Bei Erfolg kann OrderPlaced entstehen. Ein Command kann abgelehnt werden, ein Event ist eine Tatsache, die nicht mehr geändert wird.

Broker liefern häufig at least once. Consumer müssen deshalb die Event-ID speichern oder ihre Operation natürlich idempotent gestalten.

if processedEvents.contains(event.id): return
applyProjection(event)
markProcessed(event.id)

Die Kosten sind typischerweise ein O(1)-Lookup im Mittel oder O(log n) mit B-Tree. Die operativen Kosten einer doppelten Bestellung sind deutlich höher.

Warum das Design so aussieht

PlaceOrder  → eine Absicht.
OrderPlaced → eine geschehene Tatsache.

Wir machen diese Unterscheidung, weil ein Command fehlschlagen kann, ein Event jedoch eine abgeschlossene Tatsache ist, auf die andere Systeme sicher reagieren können.

  • Outbox ist nötig, weil Broker und Datenbank keine gemeinsame ACID Transaction teilen.
  • Eine Projektion ist nötig, weil Admin Panel, Mobile App, Dashboard und Analytics dieselbe Bestellung in verschiedenen Formen lesen wollen, ohne das Aggregate zu laden.
  • Idempotenz ist nötig, weil ein Broker für zuverlässige Zustellung dieselbe Nachricht erneut senden kann.
  • Eine Saga ist nötig, weil Inventory bei fehlgeschlagenem Payment im E-Commerce nicht dauerhaft reserviert bleiben darf.
  • Die große Mehrheit der Anwendungen mit CQRS verwendet kein Event Sourcing; es sind unabhängige Entscheidungen.

Was ändert sich über eine Service-Grenze hinweg?

In einer Anwendung können Transaction, Datenbank-Write und Side Effects oft lokal koordiniert werden. Sobald das System verteilt ist, besitzen Inventory, E-Mail und Reporting eigene Lebenszyklen. Ein synchroner HTTP-Aufruf kann zunächst einfach wirken; jeder weitere Aufruf fügt jedoch Latenz, Fehlerfortpflanzung und Druck zum gemeinsamen Deploy hinzu.

Ein eventgetriebener Flow verschiebt diese Abhängigkeit in einen Datenvertrag. Der Order-Service besitzt das Schema von OrderPlaced; der Inventory-Service konsumiert nur den Teil, den er braucht. Der Producer kennt weder Datenbank noch Verfügbarkeit des Consumers. Independent Deployability ohne Independent Observability verschiebt Risiko nur an eine andere Stelle.

Die Transaction-Grenze sicher verlassen

Der naive Flow lautet:

Save order → Commit → Publish OrderPlaced

Wenn der Commit gelingt und das Publish fehlschlägt, existiert die Bestellung, andere Services wissen aber nichts davon. Erst zu publizieren und dann beim Commit zu scheitern, erzeugt ein Ghost Event. Das Outbox Pattern schreibt beide Fakten in eine lokale Transaction:

Transaction
  → Order schreiben
  → OrderPlaced in die Outbox schreiben
  → Commit

Relay
  → Outbox-Eintrag an Broker liefern
  → als zugestellt markieren

Outbox baut keine verteilte Transaction. Es garantiert: Liegt die Bestellung in der Datenbank, existiert auch ein zu publizierendes Event. Das Relay darf wiederholen; daher bleibt Idempotenz auf Consumer-Seite zwingend.

CDC kann Änderungen aus einem Datenbank-Log erfassen. Das ist stark, um bestehende Datenänderungen zu verbreiten, gibt der Anwendung aber weniger Kontrolle darüber, welche Änderung fachliche Bedeutung trägt. Outbox macht diese Wahl explizit.

Frage CDC Outbox
Events in Domain-Sprache wählen Begrenzt Explizit
Bindung an die Application Transaction Indirekt Direkt
Consumer-Idempotenz erforderlich Ja Ja

Projektion: keine Kopie, sondern eine absichtsvolle Sicht

Ein Read Model ist keine unvollständige Kopie des Write Models. ProductSearchRow kann eine Produktliste bedienen; OrderFulfilmentSummary einen Operations-Screen. Dasselbe Event kann unterschiedliche Projektionen für unterschiedliche Teams versorgen.

OrderPlaced → OrderSummaryProjection → { orderId, customerName, total, status }
OrderPlaced → InventoryProjection → { sku, reservedQuantity, availability }

Projektionen sollten neu aufbaubar sein. Wenn sich Code ändert oder ein Fehler behoben wird, wird der Event Stream kontrolliert erneut abgespielt und das neue Read Model validiert. Dafür braucht es Reihenfolge, Checkpoints, Versionierung und gemessene Replay-Geschwindigkeit. Darf eine Bestellung erst einige Sekunden später in einer Liste erscheinen? Die Antwort ist eine Geschäftsentscheidung.

Ein Broker ist keine Markenentscheidung

Ein Broker liefert Garantien für Reihenfolge, Persistenz, Consumer Groups, Replay und Failure Queues. Wenn Reihenfolge für einen Schlüssel wichtig ist, müssen Partition Key und Reihenfolge-Garantie entworfen werden. Wenn Consumer zurückfallen können, müssen Lag sowie Retry- und Dead-Letter-Verhalten überwacht werden. Wenn sich Schemas ändern, müssen Event-Versionen rückwärtskompatibel bleiben.

Event Sourcing und CQRS sind nicht dasselbe

CQRS trennt Lese- und Schreibverantwortung. Event Sourcing leitet Aggregate-Zustand aus einem append-only Event Stream statt aus einer aktuellen Zeile ab. Beides lässt sich kombinieren, doch CQRS verlangt kein Event Sourcing und Event Sourcing verlangt kein Kafka. Bei Event Sourcing brauchen Snapshots, Stream-Versionen und Replay-Kosten ein eigenes Design.

Saga: die Kompensationsentscheidung im verteilten Workflow

Eine Bestellung kann Payment, Inventory und Shipment durchlaufen; diese Schritte passen nicht in eine ACID Transaction. Eine Saga definiert für jede lokale Transaction einen Kompensationsschritt.

Order placed → reserve inventory → capture payment → create shipment
payment fails → release inventory → mark order failed

Choreography lässt Services sich über Events auslösen; lokale Kopplung ist gering, doch der gesamte Ablauf ist schwerer zu verfolgen. Orchestration macht den Flow über einen zentralen Process Manager sichtbar, fügt aber Koordinationsverantwortung hinzu.

Operative Checkliste

  1. Ist jedes Event in Vergangenheit und Geschäftssprache benannt?
  2. Schließt Outbox die Lücke zwischen Datenbank-Write und Event-Publish?
  3. Ist jeder Consumer bei Duplicates, Reordering und verspäteten Events sicher?
  4. Sind Projection Checkpoints, Lag und der Rebuild-Prozess beobachtbar?
  5. Sind Owner, Versionsstrategie und Rückwärtskompatibilitätsregel jedes Event Contracts klar?
  6. Hat das Produkt das sichtbare Eventual-Consistency-Fenster akzeptiert?

Ohne diese Antworten kann ein System event-driven aussehen und sich doch failure-driven verhalten.

Häufig verwechselte Unterschiede

❌ Event = Command
✓ Ein Command ist Absicht; ein Event ist eine geschehene Tatsache.

❌ Broker = Event Store
✓ Ein Broker transportiert Events; ein Event Store kann die dauerhafte Quelle der Domain-Historie sein.

❌ Projektion = Cache
✓ Ein Cache ist temporär für Geschwindigkeit; eine Projektion ist ein bewusstes Read Model für eine Geschäfts-Query.

❌ At-least-once = Fehler
✓ Wiederzustellung ist normal; Consumer müssen idempotent sein.

Ein echter End-to-End-Flow

POST /orders
  → Controller → Mediator.Send()
  → Validation + Authorization → Transaction Behavior
  → PlaceOrderHandler → Order Aggregate → Order + Outbox committen
  → Relay → Broker → Projection Consumer → Read Database
  → GET /orders → Query Handler → OrderSummaryDto → Frontend

Wann sollte ich dieses Modell einsetzen?

Beginnen Sie mit logischem CQRS: Commands in Geschäftssprache benennen, Queries für den Screen formen und die Transaction-Grenze sichtbar machen. Broker und getrennte Projektionen kommen erst hinzu, wenn unabhängige Consumer, asymmetrische Read-Last oder replaybare Integrationsbedürfnisse nachgewiesen sind.

Jeder neue Consumer kann wie eine O(1)-Codeänderung aussehen, seine operativen Kosten sind jedoch nicht konstant: Contract, Dashboard, Alarm, Retry-Strategie, Owner und Testszenarien sind nötig.

Reflection

Das Versprechen von Distributed CQRS sind nicht mehr Nachrichten, sondern sichtbarere Verantwortungen. Events tragen Historie, der Broker trägt den Flow und Projektionen tragen das Ergebnis, das ein Nutzer sieht.

Ein Event-Flow wird erst dann zu einer Architekturentscheidung, wenn er sicher bleibt, wenn Nachrichten doppelt, verspätet und erneut abgespielt eintreffen.

Im nächsten Kapitel untersuchen wir, was sich ändert, wenn das System in Production bricht: Konsistenz, Duplicates, beschädigte Projektionen und Recovery-Strategien.

Was sollte im Gedächtnis bleiben?

Wenn Sie nur fünf Dinge behalten:

  1. Ein Command fordert Verhalten an.
  2. Ein Event ist eine geschehene Tatsache.
  3. Ein Broker trägt Events zu den richtigen Consumern.
  4. Eine Projektion liest dieselbe Tatsache in der Form, die jeder Screen braucht.
  5. Outbox verhindert Event-Verlust, weil Broker und Datenbank keine gemeinsame ACID Transaction teilen.

Jede Trennung in diesem Kapitel hat einen Grund: Idempotenz ist nötig, weil ein Broker erneut zustellen kann; eine Projektion ist nötig, weil ein Aggregate für jeden Screen zu lesen teuer und die falsche Abstraktion ist.

FAQ

Häufige Fragen

Was ist Domain Event?

Eine unveränderliche Geschäfts-Tatsache, im Präteritum benannt.

Was ist Broker?

Die Kommunikationsschicht, die eine Nachricht dauerhaft an Consumer verteilt.

Stimmt es, dass „Event = Command“?

Ein Command ist Absicht; ein Event ist eine geschehene Tatsache.

Gelernte Engineering-Prinzipien

  • In verteilten Systemen ist ein Event kein Command, sondern eine unveränderliche Geschäfts-Tatsache.
  • At-least-once-Zustellung verlangt idempotente Consumer; Duplicates sind ein Design-Eingang, keine Ausnahme.
  • Eine Projektion muss neu aufbaubar und messbar sein; ihre Latenz braucht eine Definition in Produktsprache.

Weiterlesen

Weiterlesen

Verwandte Beitrage

Verwandte Beitrage

Verwandte Beitrage

Paylaş