Wann CQRS und Event Sourcing zu viel sind
Für die meisten Systeme reicht CRUD. CQRS und Event Sourcing lohnen sich nur unter klaren Bedingungen – dieser Beitrag liefert einen ehrlichen Entscheidungsleitfaden statt Muster als Selbstzweck.
Wann CQRS und Event Sourcing zu viel sind
In Schulungen und Reviews sehe ich ein wiederkehrendes Muster: Ein Team hat von CQRS und Event Sourcing gehört, findet die Idee überzeugend – und plant plötzlich für jede Entität einen Command-Handler, einen Event Store und eine Projektion. Wenige Wochen später steckt die Hälfte der Zeit in Infrastruktur, die niemand angefordert hat. Deshalb schreibe ich diesen Beitrag als bewusste Gegenposition zu meinen sonstigen Texten über das Thema. Ich mag beide Muster. Aber für die meisten Systeme sind sie zu viel.
Das ist keine steile These. Martin Fowler formuliert es fast identisch: "For most systems CQRS adds risky complexity." Und ungewöhnlich deutlich für ihn: In der Mehrzahl der Fälle, die ihm begegnet sind, war CQRS keine gute Wahl, sondern brachte ein erhebliches Risiko mit, das System in ernste Schwierigkeiten zu bringen. Wenn der Autor, der ein Muster bekannt gemacht hat, so warnt, lohnt sich ein ehrlicher Entscheidungsleitfaden.
Die Baseline heißt CRUD
Bevor ich über Alternativen rede, halte ich fest, was die Standardlösung eigentlich leistet. CRUD bedeutet: ein einziges Datenmodell für Lesen und Schreiben, direkte Persistenz des aktuellen Zustands, meist gegen eine Tabelle über ein ORM. Create, Read, Update, Delete. Fowler nennt das nüchtern: "Many systems do fit a CRUD mental model, and so should be done in that style."
Der entscheidende Punkt ist, dass CRUD nicht die primitive Vorstufe ist, die man später durch etwas Besseres ersetzt. Für ein Formular, das gespeichert und wieder angezeigt wird, ist CRUD nicht der Kompromiss, sondern die richtige Antwort. Ein illustrativer Handler dafür ist unspektakulär – und genau das ist die Stärke:
async function updateCustomerEmail(id: string, email: string): Promise<void> {
const customer = await repo.findById(id);
customer.email = email;
await repo.save(customer); // read and write share the same model
}
Ein Modell, ein Zugriff, keine Latenz zwischen Schreiben und Lesen. Wer diesen Wert wegoptimiert, ohne ein Problem zu haben, tauscht Einfachheit gegen Komplexität ein und bekommt dafür nichts zurück.
Was CQRS und Event Sourcing wirklich sind
Damit die Entscheidung sauber wird, trenne ich die beiden Begriffe, die gern in einem Atemzug genannt werden. CQRS – Command Query Responsibility Segregation – heißt, dass man ein anderes Modell zum Ändern von Daten benutzt als zum Lesen. Die Schreibseite nimmt Commands an, prüft Invarianten und gibt nichts zurück. Die Leseseite beantwortet Queries, liefert Daten und ändert nichts. Das ist mehr als das Command-Query-Separation-Prinzip von Bertrand Meyer: CQS trennt Methoden auf einem Objekt, CQRS trennt ganze Modelle.
Event Sourcing ist etwas anderes. Hier wird jede Zustandsänderung als Event festgehalten, und der aktuelle Zustand entsteht durch erneutes Abspielen der Events. Der Event Store ist die maßgebliche Quelle der Wahrheit, der Zustand ist rein abgeleitet. Fowlers Analogie dazu ist die Versionskontrolle: Das Commit-Log entspricht dem Event Store, der Arbeitsbaum dem aktuellen Systemzustand. Diesen Gedanken habe ich in Event Sourcing: der Event-Store als Logbuch ausführlicher entwickelt.
Wichtig für den heutigen Beitrag: Beide Muster sind unabhängig voneinander. Greg Young stellt das mehrfach klar – CQRS ist nicht Event Sourcing, braucht keinen Message Bus und ist auch nicht zwingend an DDD gekoppelt. Man kann CQRS ohne Event Sourcing betreiben, also mit getrennter Lese-Datenbank, aber klassischer Zustandspersistenz. Und man kann Event Sourcing mit nur einem Modell betreiben. Wer beide zusammen einführt, entscheidet sich also für zwei Kostenblöcke, nicht für einen.
Die Kosten, die man selten mitplant
Getrennte Modelle erzwingen faktisch Eventual Consistency. Sobald die Schreibseite ein Event erzeugt und die Leseseite daraus erst eine Projektion baut, existiert ein Zeitfenster, in dem eine Query den gerade geschriebenen Wert noch nicht sieht. Fowler nennt das offen: Getrennte Modelle erhöhen die Wahrscheinlichkeit, Eventual Consistency einsetzen zu müssen. Das ist keine Funktion, sondern eine Kostenposition.
Ein illustrativer Kontrast zum CRUD-Handler oben zeigt die zusätzlichen beweglichen Teile. Erst die Nachrichten:
interface ChangeEmail {
type: "ChangeEmail";
id: string;
email: string;
}
interface EmailChanged {
type: "EmailChanged";
id: string;
email: string;
version: number;
}
Dann das Write Model, das aus seinen Events rehydriert wird und beim Ändern eine Invariante prüft, statt einfach ein Feld zu überschreiben:
class Customer {
private constructor(
private readonly id: string,
private email: string,
private version: number,
) {}
static empty(): Customer {
return new Customer("", "", 0);
}
static from(events: EmailChanged[]): Customer {
// fold the event stream into current state (replay)
return events.reduce(
(state, event) => state.apply(event),
Customer.empty(),
);
}
private apply(event: EmailChanged): Customer {
return new Customer(event.id, event.email, event.version);
}
changeEmail(email: string): EmailChanged[] {
if (email === this.email) return []; // no change, no event
return [{ type: "EmailChanged", id: this.id, email, version: this.version + 1 }];
}
}
Und schließlich der Handler, der lädt, entscheidet, anhängt – und die Projektion getrennt davon nachzieht:
async function handle(command: ChangeEmail): Promise<void> {
const events = await store.load(command.id);
const customer = Customer.from(events);
const newEvents = customer.changeEmail(command.email);
await store.append(command.id, newEvents); // source of truth
// read model is updated asynchronously by a projection
}
Die Leseseite ist ein eigenes, bewusst dummes Read Model, das aus denselben Events entsteht:
class CustomerListView {
private rows = new Map<string, { id: string; email: string }>();
on(event: EmailChanged): void {
this.rows.set(event.id, { id: event.id, email: event.email });
}
// a query right after append may still return the previous email (stale read)
find(id: string) {
return this.rows.get(id);
}
}
Zwischen dem ersten CRUD-Beispiel und dieser Variante liegt kein Fortschritt, sondern eine Abwägung. Ich habe mehr Typen, mehr Indirektion, einen Replay-Pfad, eine Projektion und ein Konsistenzfenster eingetauscht – gegen einen Nutzen, der nur unter bestimmten Bedingungen überhaupt entsteht.
Event Sourcing bringt darüber hinaus eigene, oft unterschätzte Kosten mit. Alte Events müssen praktisch für immer lesbar bleiben, was Event-Versionierung und Schema-Evolution zu einer Daueraufgabe macht. Projektionen und Read Models müssen aufgebaut und neu aufgebaut werden können. Externe Seiteneffekte beim Replay wollen sorgfältig behandelt sein, sonst verschickt ein Neuaufbau versehentlich echte E-Mails. Und ein einfaches Ad-hoc-Query auf den aktuellen Zustand gibt es nicht mehr geschenkt. Diese Punkte kommen zusätzlich zu den CQRS-Kosten obendrauf.
Ein Entscheidungsfluss
Ich mache die Entscheidung in Schulungen an wenigen Fragen fest, in dieser Reihenfolge. Wichtig dabei: Das gilt pro Bounded Context, nicht global. Greg Young ist hier eindeutig – CQRS ist kein Top-Level-Muster, es wird innerhalb einer Komponente angewandt, nicht über das ganze System gestülpt.
flowchart TD
Start([Neuer Context]) --> Inv{Echtes Domänenmodell<br/>mit kollidierenden Invarianten?}
Inv -->|nein| Crud[CRUD genügt]
Inv -->|ja| Asym{Starke Read/Write-Asymmetrie<br/>ODER Audit-/Zeitreise-Anforderung?}
Asym -->|nein| Lite[Weiter CRUD<br/>oder CQRS-lite]
Asym -->|ja| Team{Team trägt Eventual Consistency<br/>und Event-Versionierung?}
Team -->|nein| Kompromiss[CRUD oder Kompromiss]
Team -->|ja| Cqrs[CQRS – plus Event Sourcing,<br/>wenn Audit oder Temporal]
Der Nutzen von CQRS ist eng an zwei Bedingungen gebunden. Erstens eine komplexe Domäne, in der ein gemeinsames Modell für Lesen und Schreiben tatsächlich zu kompliziert wird. Zweitens eine ausgeprägte Lese-/Schreib-Asymmetrie – viele Lesezugriffe, wenige Schreibzugriffe –, sodass sich die unabhängige Skalierung von Lese- und Schreibseite auszahlt. Fehlen beide, führt Fowlers Beobachtung geradewegs in die Falle: CQRS auf einer Domäne, die nicht dazu passt, erhöht die Komplexität, senkt die Produktivität und steigert das Risiko. Das Muster vereinfacht also nicht – es vereinfacht nur dort, wo bereits echte Modell- oder Invarianten-Komplexität liegt.
Signale dafür und dagegen
Damit die Abwägung greifbar bleibt, hier die Signale, an denen ich sie festmache.
Dafür spricht:
- ein task-basiertes UI mit vielen nebenläufig Zusammenarbeitenden, deren Aktionen fachlich kollidieren können
- harte Audit-, Compliance- oder Zeitreise-Anforderungen, die eine lückenlose Historie verlangen – hier zieht besonders Event Sourcing
- eine ausgeprägte Read/Write-Asymmetrie mit getrennten Skalierungszielen
- eine reichhaltige Domäne mit echten, konkurrierenden Invarianten und deutlich unterschiedlichen Lese- gegenüber Schreib-Formen
Dagegen spricht:
- eine klassische CRUD- oder Formular-Anwendung ohne echte Invarianten-Komplexität
- ein kleines Team, das mit Eventual Consistency noch keine Erfahrung hat
- keine Audit- oder Historien-Anforderung, die den Aufwand rechtfertigt
- der Wunsch nach einer globalen Einheitsarchitektur – oder schlicht "weil es alle machen"
Der letzte Punkt führt zum eigentlichen Fallstrick.
Der Fallstrick: das Muster als Selbstzweck
Das größte Risiko ist nicht die falsche Technik, sondern die falsche Motivation. Greg Young beschreibt den Kern präzise: Teams wollen CQRS und Event Sourcing überall implementieren, als Cookie-Cutter-Architektur – und genau das ist ein massives Risiko. Ein Muster wird eingeführt, weil es glänzt, nicht weil ein konkretes Problem danach verlangt. Fowler rät zur Vorsicht, weil das Muster schwer gut einzusetzen ist.
In Reviews erkenne ich diesen Cargo Cult an drei Symptomen. Es gibt Events, aber niemand kann eine fachliche Invariante nennen, die sie schützen. Es gibt eine Projektion, aber die Leseform ist identisch zur Schreibform. Und es gibt einen Event Store, aber keine Anforderung an Audit oder Historie. Wenn diese drei Dinge zusammenkommen, ist das Muster dekorativ, nicht tragend.
Dazu kommen die üblichen Verwechslungen, die ich immer wieder geradeziehe. CQRS ist nicht Event Sourcing – beide sind orthogonal und einzeln einsetzbar. CQRS ist keine Architektur und kein Top-Level-Muster, sondern eine Entscheidung innerhalb eines Bounded Context. CQRS ist nicht CQS, weil es Modelle trennt und nicht Methoden. CQRS braucht weder Message Bus noch DDD. Und Event Sourcing ist nicht dasselbe wie ereignisgetriebene Integration über Pub/Sub – es ist ein interner Persistenz- und Source-of-Truth-Mechanismus, kein Kommunikationsstil zwischen Systemen. Fowler trennt diese Bedeutungen von "Event-Driven" bewusst, weil sonst vier verschiedene Dinge unter einem Wort verschwimmen.
Mehr bewegliche Teile, sichtbar gemacht
Wenn ich einem Team die Entscheidung erklären will, hilft ein direkter Kontrast der Pfade. Der CRUD-Pfad ist kurz, der CQRS-Pfad mit Event Sourcing hat mehr Stationen – und jede Station ist ein Ort, an dem etwas schiefgehen kann.
flowchart LR
subgraph CRUD
C1[Client] --> H1[Handler] --> DB[(Database)]
end
subgraph CQRS_ES
Cmd[Command] --> Agg[Aggregate] --> ES[(Event Store)]
ES --> Proj[Projection] --> RM[(Read Model)]
RM --> Q[Query]
end
Diese Grafik verkauft nichts. Sie macht nur ehrlich, dass die untere Variante mehr Komponenten, mehr Verträge und mehr Fehlerpfade bedeutet. Wer diesen Preis kennt und trotzdem zahlt, weil die Domäne ihn wert ist, trifft eine gute Entscheidung. Wer ihn zahlt, ohne den Gegenwert zu haben, baut Komplexität auf Vorrat.
Fazit
CQRS und Event Sourcing sind keine besseren Standardarchitekturen, sondern gezielte Werkzeuge für gezielte Probleme. Die ehrliche Ausgangslage ist CRUD, und für den Großteil der Systeme bleibt sie die richtige Antwort. Die beiden Muster lohnen sich, wenn eine Domäne echte, kollidierende Invarianten hat, wenn Lese- und Schreiblast stark auseinanderlaufen oder wenn Audit- und Zeitreise-Anforderungen eine lückenlose Historie verlangen – und wenn das Team bereit ist, Eventual Consistency und Event-Versionierung zu tragen. Sie werden zum Overhead bei einfachen CRUD-Apps, in kleinen Teams, ohne echte Invarianten-Komplexität und immer dann, wenn das Muster aus Mode statt aus Not gewählt wird.
Der beste Umgang damit ist eine bewusste, kontextlokale Entscheidung. Nicht global, nicht als Cookie-Cutter, sondern pro Bounded Context, mit einem klaren "Wofür". Dass CQRS diese fachliche Grenze braucht und kein Framework ist, habe ich in CQRS ist eine fachliche Grenze beschrieben. Die reifste Architekturqualität zeigt sich oft nicht darin, ein anspruchsvolles Muster einzusetzen – sondern darin, es begründet nicht einzusetzen.
Weiterführende Quellen
- Martin Fowler, CQRS: https://martinfowler.com/bliki/CQRS.html
- Martin Fowler, Event Sourcing: https://martinfowler.com/eaaDev/EventSourcing.html
- Martin Fowler, Command Query Separation: https://martinfowler.com/bliki/CommandQuerySeparation.html
- Martin Fowler, What do you mean by Event-Driven?: https://martinfowler.com/articles/201701-event-driven.html
- Greg Young, CQRS is not an Architecture (2012): https://gregfyoung.wordpress.com/2012/09/09/cqrs-is-not-an-architecture/
- Greg Young, CQRS (2012): https://gregfyoung.wordpress.com/2012/03/02/cqrs/
- Vaughn Vernon, Implementing Domain-Driven Design (Sample): https://ptgmedia.pearsoncmg.com/images/9780321834577/samplepages/0321834577.pdf
- Eigenes Vortragsmaterial "CQRS + Event Sourcing" (SlideShare)
Die Quellen sichern die Begriffe gegen Standards ab. Die eigentliche Aussage bleibt die kontextlokale Entscheidung, das Muster nur dort einzusetzen, wo die Domäne es verdient.
Kommentare