Lesemodelle als GraphQL-Schema modellieren
Wenn die Schreibseite eines Systems in einem Event Store lebt, muss die Leseseite nicht dasselbe Datenmodell verwenden. GraphQL und Event Sourcing passen erstaunlich gut zusammen, weil GraphQL-Queries genau das leisten, was CQRS-Lesemodelle brauchen, flexible, projektionsbasierte Abfragen ohne den Event-Strom selbst zu berühren.
Inhaltsverzeichnis
- 1. Warum GraphQL und CQRS sich natürlich ergänzen
- 2. Der Event Store als einzige Quelle der Wahrheit
- 3. Projektionen: vom Event-Strom zum Lesemodell
- 4. GraphQL-Schema-Design für projizierte Lesemodelle
- 5. Mutations als Command-Dispatch statt direkter Schreibung
- 6. Eventual Consistency dem Client erklären und sichtbar machen
- 7. Subscriptions: Live-Updates direkt aus dem Event-Strom
- 8. Projektionen neu aufbauen ohne Downtime
- 9. GraphQL-über-CQRS vs. GraphQL-über-CRUD im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum GraphQL und CQRS sich natürlich ergänzen
Command Query Responsibility Segregation trennt Schreiboperationen, Commands, strikt von Leseoperationen, Queries, oft mit vollständig getrennten Datenmodellen für beide Seiten. Event Sourcing ergänzt dieses Muster, indem die Schreibseite keinen aktuellen Zustand speichert, sondern ausschließlich die Abfolge aller Domain-Events, aus denen sich der aktuelle Zustand jederzeit neu berechnen lässt. GraphQL und Event Sourcing passen konzeptionell gut zusammen, weil GraphQL selbst schon eine strikte Trennung zwischen Query und Mutation im Typsystem vorschreibt.
Der entscheidende Vorteil zeigt sich beim Lesemodell: Statt eine einzige, generische Datenbanktabelle für alle Lesezugriffe zu pflegen, wie es bei klassischem CRUD üblich ist, erlaubt CQRS beliebig viele spezialisierte Lesemodelle, jedes optimiert für einen bestimmten Zugriffsmuster. Ein GraphQL-Schema kann diese Vielfalt an Lesemodellen elegant als unterschiedliche Felder und Types exponieren, ohne dass der Client wissen muss, aus wie vielen unterschiedlichen physischen Datenquellen die Antwort tatsächlich zusammengesetzt wird.
2. Der Event Store als einzige Quelle der Wahrheit
Im Zentrum jedes Event-Sourcing-Systems steht der Event Store, ein Append-only-Log aller jemals aufgetretenen Domain-Events, etwa OrderPlaced, PaymentCaptured, OrderShipped. Anders als bei klassischer CRUD-Persistenz wird niemals ein bestehender Datensatz überschrieben, jede Zustandsänderung entsteht als neues, unveränderliches Event am Ende des Logs. Dieser Event Store ist die einzige verbindliche Quelle der Wahrheit im gesamten System.
Für GraphQL und Event Sourcing bedeutet das: Der GraphQL-Layer greift niemals direkt auf den Event Store zu, um Queries zu beantworten. Der Event Store ist zu granular und zu langsam für typische Lesezugriffe, eine Bestellliste mit hundert Einträgen würde bedeuten, hunderte Event-Streams zur Anfragezeit neu abzuspielen. Stattdessen dienen Events ausschließlich als Input für Projektionen, die das eigentliche, für GraphQL-Queries optimierte Lesemodell erzeugen.
// Example domain events in the event store, append-only, immutable
{ "eventType": "OrderPlaced", "orderId": "ORD-9931", "sku": "SKU-4821", "quantity": 2, "occurredAt": "2026-08-06T09:00:00Z" }
{ "eventType": "PaymentCaptured", "orderId": "ORD-9931", "amount": 79.98, "occurredAt": "2026-08-06T09:00:04Z" }
{ "eventType": "OrderShipped", "orderId": "ORD-9931", "trackingId": "TRK-5512", "occurredAt": "2026-08-07T14:30:00Z" }
3. Projektionen: vom Event-Strom zum Lesemodell
Eine Projektion ist ein Event-Handler, der den Event-Strom liest und daraus ein denormalisiertes, abfrageoptimiertes Lesemodell in einer eigenen Tabelle oder einem eigenen Dokument pflegt. Für dieselbe Domäne können mehrere unterschiedliche Projektionen gleichzeitig existieren, eine für die Bestellübersicht im Kundenkonto, eine andere für das Fulfillment-Dashboard im Lager, eine dritte für Reporting-Zwecke. Jede Projektion ist auf genau einen Lesezweck zugeschnitten, statt einen universellen Kompromiss aus allen Anforderungen darzustellen.
Für GraphQL und Event Sourcing ist entscheidend, dass jeder GraphQL-Query-Resolver genau einer Projektion zugeordnet wird, niemals dem Event Store direkt. Diese Zuordnung macht den Resolver trivial einfach: Er ist im Wesentlichen ein einfacher Datenbank-Lookup gegen eine bereits vorberechnete Tabelle, ohne Domänenlogik, ohne Event-Replay zur Anfragezeit. Die gesamte Komplexität der Zustandsberechnung wandert vollständig in die Projektion, weit weg vom GraphQL-Layer.
<?php
declare(strict_types=1);
namespace App\Projection\OrderSummary;
use App\Domain\Event\OrderPlaced;
use App\Domain\Event\OrderShipped;
use App\Domain\Event\PaymentCaptured;
/**
* Projects domain events into a denormalized, query-optimized
* read model dedicated to the customer account order overview.
*/
final class OrderSummaryProjection
{
public function __construct(
private readonly OrderSummaryWriteRepository $repository,
) {
}
/**
* Handles an OrderPlaced event and inserts a new summary row.
*
* @param OrderPlaced $event
* @return void
*/
public function onOrderPlaced(OrderPlaced $event): void
{
$this->repository->insert([
'order_id' => $event->orderId,
'status' => 'PLACED',
'sku' => $event->sku,
'quantity' => $event->quantity,
]);
}
/**
* Handles a PaymentCaptured event and updates the summary status.
*
* @param PaymentCaptured $event
* @return void
*/
public function onPaymentCaptured(PaymentCaptured $event): void
{
$this->repository->updateStatus($event->orderId, 'PAID');
}
/**
* Handles an OrderShipped event and updates status plus tracking.
*
* @param OrderShipped $event
* @return void
*/
public function onOrderShipped(OrderShipped $event): void
{
$this->repository->updateShipment($event->orderId, 'SHIPPED', $event->trackingId);
}
}
4. GraphQL-Schema-Design für projizierte Lesemodelle
Ein häufiger Designfehler bei GraphQL und Event Sourcing ist, das GraphQL-Schema direkt aus den Domain-Events abzuleiten, mit einem Feld pro Event-Typ. Das erzeugt ein technisches, für Clients unbrauchbares Schema. Richtig ist, das Schema strikt aus Sicht der Projektionen zu entwerfen, also aus Sicht dessen, was der Client tatsächlich lesen möchte, unabhängig davon, aus welchen Events sich dieser Zustand ursprünglich zusammensetzt.
Da unterschiedliche Projektionen unterschiedliche Sichten auf dieselbe fachliche Entität liefern können, muss das Schema diese Vielfalt bewusst abbilden, statt sie künstlich in ein einziges Objekt zu zwingen. Eine Bestellung im Kundenkonto-Kontext braucht andere Felder als dieselbe Bestellung im Fulfillment-Kontext. Zwei getrennte GraphQL-Types, die beide letztlich aus demselben Event-Strom gespeist werden, sind hier oft sauberer als ein einziger, überladener Order-Type.
# Schema reflects the projection, not the raw event stream -
# each type maps to exactly one dedicated read model
type CustomerOrderSummary {
orderId: ID!
status: OrderStatus!
sku: String!
quantity: Int!
}
type FulfillmentOrder {
orderId: ID!
status: OrderStatus!
warehouseZone: String!
pickListPriority: Int!
}
type Query {
# Reads exclusively from the customer-facing projection
myOrders(customerId: ID!): [CustomerOrderSummary!]!
# Reads exclusively from the warehouse-facing projection
fulfillmentQueue(zone: String!): [FulfillmentOrder!]!
}
5. Mutations als Command-Dispatch statt direkter Schreibung
In einem CQRS-System schreibt eine GraphQL-Mutation niemals direkt in eine Lesemodell-Tabelle. Stattdessen übersetzt der Mutation-Resolver den GraphQL-Input in ein Command, ein fachlich benanntes Objekt wie PlaceOrderCommand, das an den Command-Handler der Domäne weitergereicht wird. Der Handler validiert das Command gegen den aktuellen, aus Events rekonstruierten Aggregatzustand und erzeugt bei Erfolg ein oder mehrere neue Domain-Events im Event Store.
Diese Indirektion ist bei GraphQL und Event Sourcing kein unnötiger Umweg, sondern der eigentliche Kern des Musters: Die Mutation weiß nichts über Lesemodelle, sie kennt nur Commands und deren unmittelbares Ergebnis. Die Aktualisierung der Lesemodelle geschieht vollständig asynchron und entkoppelt, ausgelöst durch die neu geschriebenen Events, nicht durch die Mutation selbst.
6. Eventual Consistency dem Client erklären und sichtbar machen
Die unmittelbare Konsequenz aus Abschnitt 5: Direkt nach einer erfolgreichen Mutation kann eine nachfolgende Query noch den alten Zustand liefern, weil die zuständige Projektion das neue Event noch nicht verarbeitet hat. Diese Verzögerung ist meist im niedrigen einstelligen Millisekundenbereich, aber sie existiert, und Clients müssen sie berücksichtigen. Wer GraphQL und Event Sourcing produktiv einsetzt, darf diese Eventual Consistency niemals verschweigen, sondern muss sie explizit im Schema sichtbar machen.
Ein bewährtes Muster: Die Mutation-Response enthält nicht nur die ID der neuen Entität, sondern auch eine Sequenznummer oder einen Timestamp des auslösenden Events. Ein nachfolgender Query-Aufruf kann diesen Wert als Parameter mitgeben und den Server damit anweisen, erst zu antworten, sobald die Projektion mindestens diesen Event-Stand erreicht hat. Dieses als Read-Your-Writes bekannte Muster löst das UX-Problem verzögerter Konsistenz, ohne auf synchrone, tightly gekoppelte Schreibpfade zurückzufallen.
# Mutation returns the triggering event's sequence number,
# so the client can request read-your-writes consistency
type PlaceOrderPayload {
orderId: ID!
eventSequence: Int!
}
type Query {
# Waits until the projection has caught up to at least this sequence
myOrders(customerId: ID!, minEventSequence: Int): [CustomerOrderSummary!]!
}
7. Subscriptions: Live-Updates direkt aus dem Event-Strom
GraphQL Subscriptions sind ein natürlicher Partner für Event Sourcing, weil beide Konzepte auf demselben Grundgedanken beruhen, Zustandsänderungen als diskrete, zeitlich geordnete Ereignisse zu behandeln. Ein Subscription-Resolver kann direkt an den Event-Strom eines Aggregats andocken und jedes relevante Event in nahezu Echtzeit an verbundene Clients weiterleiten, ohne Polling, ohne künstliche Verzögerung durch Batch-Projektions-Läufe.
Wichtig ist, Subscriptions bei GraphQL und Event Sourcing ebenfalls über eine leichte Projektionsschicht laufen zu lassen, statt rohe Events unverändert an den Client durchzureichen. Der Client interessiert sich für den fachlichen Fortschritt einer Bestellung, nicht für die interne Event-Struktur des Systems. Diese Übersetzungsschicht entkoppelt das öffentliche GraphQL-Schema von internen Event-Schema-Änderungen, die sonst zu Breaking Changes für alle Subscriber führen würden.
8. Projektionen neu aufbauen ohne Downtime
Ein praktischer Vorteil von Event Sourcing zeigt sich, sobald sich Anforderungen an ein Lesemodell ändern: Eine Projektion lässt sich jederzeit von Grund auf neu aufbauen, indem der komplette Event-Strom von Anfang an erneut abgespielt wird. Ein neues Feld im GraphQL-Schema, das aus bereits vorhandenen historischen Events ableitbar ist, erfordert keine Datenmigration im klassischen Sinne, sondern lediglich eine neue Projektionslogik und einen vollständigen Replay.
Damit dieser Rebuild ohne Downtime funktioniert, muss die neue Projektionsversion parallel zur alten aufgebaut werden, in einer eigenen Tabelle, während GraphQL-Queries weiterhin die alte Version bedienen. Erst wenn der Rebuild den aktuellen Event-Stand vollständig eingeholt hat, schaltet ein einfacher Alias- oder Feature-Flag-Wechsel die Queries auf die neue Projektion um. Wer GraphQL und Event Sourcing so betreibt, kann Lesemodelle iterieren, ohne den produktiven Query-Pfad je zu unterbrechen.
9. GraphQL-über-CQRS vs. GraphQL-über-CRUD im Vergleich
Der Architekturwechsel zu CQRS und Event Sourcing lohnt sich nicht für jedes System. Die folgende Tabelle stellt beide Ansätze im GraphQL-Kontext gegenüber.
| Kriterium | GraphQL über CRUD | GraphQL über CQRS/Event Sourcing |
|---|---|---|
| Lesekonsistenz | Sofort konsistent | Eventual Consistency, meist Millisekunden |
| Historische Nachvollziehbarkeit | Nur mit separatem Audit-Log | Vollständig, jedes Event bleibt erhalten |
| Neue Lesemodelle hinzufügen | Datenmigration nötig | Neue Projektion, Replay möglich |
| Implementierungsaufwand | Niedrig | Deutlich höher |
| Skalierung Lesezugriffe | Begrenzt durch ein Schema | Unabhängig skalierbare Projektionen |
Der zusätzliche Implementierungsaufwand von CQRS und Event Sourcing lohnt sich vor allem bei Domänen mit hohem Audit-Bedarf oder stark unterschiedlichen Lesezugriffsmustern. Für einfache CRUD-lastige Bereiche eines Systems bleibt klassisches GraphQL über einer relationalen Datenbank oft die pragmatischere Wahl, auch innerhalb eines Systems, das anderswo Event Sourcing einsetzt.
Mironsoft
GraphQL-Architektur, Event Sourcing und Magento-Integration
GraphQL über eurer CQRS- und Event-Sourcing-Architektur?
Wir entwerfen mit euch ein GraphQL-Schema, das eure Projektionen sauber abbildet, implementieren Read-Your-Writes-Konsistenz und binden Subscriptions direkt an euren Event-Strom an.
Schema-Design
GraphQL-Types entlang eurer Projektionen statt entlang roher Events entwerfen
Command-Dispatch
Mutations sauber als Command-Dispatch statt direkter Schreibung implementieren
Live-Subscriptions
Echtzeit-Updates direkt aus dem Event-Strom, entkoppelt vom internen Event-Schema
10. Zusammenfassung
GraphQL und Event Sourcing ergänzen sich, weil GraphQL bereits eine strikte Trennung zwischen Query und Mutation kennt, die sich direkt auf CQRS abbilden lässt. Der Event Store bleibt die einzige Quelle der Wahrheit, GraphQL-Queries lesen ausschließlich aus Projektionen, spezialisierten, denormalisierten Lesemodellen, die aus dem Event-Strom berechnet werden. Mutations übersetzen sich in Commands, niemals in direkte Schreibungen der Lesemodelle.
Eventual Consistency muss dem Client explizit sichtbar gemacht werden, etwa über Read-Your-Writes-Parameter mit Event-Sequenznummern. Subscriptions profitieren besonders von dieser Architektur, weil Events schon die natürliche Form von Echtzeit-Updates sind. Der zusätzliche Implementierungsaufwand lohnt sich vor allem bei hohem Audit-Bedarf oder stark unterschiedlichen Lesezugriffsmustern, nicht für jede Domäne eines Systems gleichermaßen.
GraphQL und Event Sourcing/CQRS — Das Wichtigste auf einen Blick
Event Store
Einzige Quelle der Wahrheit, Append-only, GraphQL-Queries greifen niemals direkt darauf zu.
Projektionen
Spezialisierte Lesemodelle je Zugriffsmuster, jeder Query-Resolver bedient genau eine Projektion.
Mutations als Commands
Kein direktes Schreiben ins Lesemodell, Mutations dispatchen Commands an die Domäne.
Eventual Consistency
Explizit im Schema sichtbar machen, Read-Your-Writes über Event-Sequenznummern lösen.