post

CQRS ist eine fachliche Grenze, kein Framework

CQRS trennt das Modell zum Schreiben vom Modell zum Lesen – eine fachliche Entscheidung, kein installierbares Framework. Wann sich die Grenze lohnt, wie man klein anfängt und welche Verwechslungen teuer werden.

In fast jedem Workshop, den ich zu Architektur halte, fällt irgendwann das Wort CQRS – und fast genauso oft folgt die Frage: „Welches Framework nehmen wir dafür?" Das ist der Moment, an dem ich kurz innehalte. Denn hinter der Frage steckt ein Missverständnis, das sich hartnäckig hält: die Vorstellung, CQRS sei eine Technologie, die man ins Projekt zieht wie eine Library. Ist es nicht. CQRS ist eine fachliche Entscheidung, eine Grenze, die man durch die Domäne zieht. Man installiert sie nicht, man erkennt sie.

In diesem Artikel möchte ich zeigen, was CQRS wirklich ist, was es ausdrücklich nicht ist, wann sich die Trennung lohnt und wo ich in der Praxis die teuersten Fehler sehe. Das ist unspektakulärer, als das Akronym verspricht – und genau das ist die gute Nachricht.

Woher der Begriff kommt

CQRS steht für Command Query Responsibility Segregation. Der Kern ist eine einzige Idee: Man verwendet ein anderes Modell zum Ändern von Informationen als das Modell zum Lesen. Was in klassischen Systemen ein einziges konzeptionelles Modell ist – dieselben Entitäten, mit denen man schreibt und liest –, wird bewusst in zwei getrennte Modelle gespalten. Eines für Commands, also für Absichten und Änderungen. Eines für Queries, also für Sichten und Abfragen.

Martin Fowler bringt die Begründung auf den Punkt: Für viele Probleme, besonders in komplexeren Domänen, führt ein gemeinsames konzeptionelles Modell für Commands und Queries zu einem Modell, das beide Aufgaben schlechter erfüllt, als zwei spezialisierte Modelle es täten. Fowler selbst hörte den Begriff zuerst bei Greg Young, der CQRS um 2010 prägte und ausformulierte.

Wichtig ist die Abgrenzung zu einem älteren Prinzip, mit dem CQRS gern verwechselt wird: Command Query Separation, kurz CQS, von Bertrand Meyer. CQS trennt auf Methodenebene – eine Methode ist entweder ein Command, der Zustand verändert und nichts zurückgibt, oder eine Query, die einen Wert liefert, ohne Zustand zu ändern. CQRS hebt genau dieses Prinzip auf die Architekturebene: nicht mehr zwei Arten von Methoden, sondern zwei getrennte Objektmodelle. CQRS baut konzeptionell auf CQS auf, arbeitet aber viel gröber.

Die zwei Seiten haben eine unterschiedliche Form

Der entscheidende Punkt, den ich in Schulungen immer wieder betone: Schreib- und Leseseite haben nicht nur unterschiedliche Aufgaben, sie haben eine unterschiedliche Form. Das ist keine kosmetische Beobachtung, sondern der eigentliche Grund für die Trennung.

Die Schreibseite arbeitet mit einem reichen Domänenmodell. Hier liegen die Invarianten, die fachlichen Regeln, die entscheiden, ob eine Änderung überhaupt erlaubt ist. Ein Command ist der Ausdruck einer fachlichen Absicht – imperativ formuliert, benennbar, ablehnbar. DeactivateInventoryItem ist kein Datensatz-Update, sondern eine Aussage: „Ich möchte diesen Artikel deaktivieren." Das passt hervorragend zu aufgabenorientierten Oberflächen, in denen der Nutzer Absichten äußert statt Formularfelder zu befüllen.

Die Leseseite dagegen will nichts entscheiden. Sie will anzeigen. Ihre natürliche Form sind flache, auf die Darstellung zugeschnittene DTOs – oft eine direkte Projektion dessen, was am Bildschirm erscheint. Regeln, Invarianten, Verhalten: All das braucht die Leseseite nicht. Sie zwingt man in ein reiches Domänenmodell nur, weil es zufällig schon da ist.

Genau hier setzt CQRS an. Statt beide Seiten in ein Modell zu pressen, das keine von beiden gut bedient, gibt man jeder Seite die Form, die sie verdient.

flowchart LR
    Client -->|Command| CommandHandler
    CommandHandler --> WriteModel
    WriteModel --> Store[(Store)]
    Client -->|Query| QueryService
    QueryService --> ReadModel
    ReadModel --> Store

Man beachte: Beide Pfade können denselben Store nutzen. Dazu gleich mehr – es ist wichtiger, als es aussieht.

Ein Blick auf die Schreibseite

Illustrativ, in TypeScript. Die Schreibseite nimmt einen Command entgegen, lädt das reiche Write Model, lässt es die fachliche Regel durchsetzen und speichert.

interface DeactivateInventoryItem {
  itemId: string;
  reason: string;
}

class InventoryCommandHandler {
  constructor(private readonly repo: InventoryItemRepository) {}

  handle(cmd: DeactivateInventoryItem): void {
    const item = this.repo.findById(cmd.itemId); // rich write model
    item.deactivate(cmd.reason);                 // domain rule enforced here
    this.repo.save(item);
  }
}

Das Interessante steht in der mittleren Zeile. item.deactivate(cmd.reason) ist der Ort, an dem die Domäne lebt – Verzeihung, an dem die Fachlichkeit liegt. Ob ein Artikel deaktiviert werden darf, ob ein Grund verpflichtend ist, ob ein bereits deaktivierter Artikel den Command ablehnt: Das entscheidet das Domänenobjekt, nicht der Handler. Der Command-Handler ist bewusst dünn. Er orchestriert, er entscheidet nicht.

Und ein Blick auf die Leseseite

Die Leseseite ignoriert dieses reiche Modell vollständig. Sie fragt eine auf Anzeige optimierte Sicht ab und gibt ein flaches DTO zurück.

interface InventoryListItemDto {
  id: string;
  name: string;
  isActive: boolean;
}

class InventoryQueryService {
  constructor(private readonly readDb: ReadDatabase) {}

  listActiveItems(): InventoryListItemDto[] {
    return this.readDb.query(
      "SELECT id, name, is_active FROM inventory_view WHERE is_active = true",
    );
  }
}

Kein Repository, kein Domänenobjekt, keine Invariante. Nur eine Abfrage gegen eine Sicht, die genau das liefert, was die Liste braucht. Man könnte einwenden, dass hier rohes SQL direkt in einem Service steht – für die Leseseite ist das oft genau richtig. Es gibt nichts zu schützen, keine Regel zu wahren. Der Umweg über ein reiches Modell wäre reine Zeremonie.

Der didaktische Punkt aus meinen Workshops: Diese beiden Klassen kennen einander nicht. Sie teilen kein gemeinsames Modell. Sie sind zwei getrennte Antworten auf zwei getrennte Fragen. Das ist CQRS.

Klein anfangen – ein Store genügt

Und jetzt kommt der Teil, der die meisten überrascht. Nichts an dem, was ich gezeigt habe, verlangt zwei Datenbanken. InventoryItemRepository auf der Schreibseite und readDb auf der Leseseite können auf dieselbe Datenbank zeigen. Fowler formuliert es unmissverständlich: Die Modelle können sich denselben Datenspeicher teilen – die Datenbank dient dann als Kommunikationskanal zwischen beiden Modellen.

Das bedeutet: Man kann CQRS praktizieren, ohne einen einzigen zusätzlichen Store einzuführen. Ein reiches Schreibmodell, das gegen die Tabellen arbeitet. Eine Sicht oder ein View, den die Leseseite direkt abfragt. Beides in derselben Datenbank. Man hat die fachliche Trennung, ohne sich einen zweiten Datenspeicher und dessen Konsequenzen einzukaufen.

Ich sage das so deutlich, weil ich immer wieder Teams sehe, die CQRS für unerschwinglich halten – zu viel Infrastruktur, zu viel Betrieb. Diese Sorge basiert auf einer Verwechslung: CQRS mit einer bestimmten, aufwendigen Ausbaustufe von CQRS gleichzusetzen. Die Trennung der Modelle ist billig. Was Geld kostet, sind getrennte Stores, und die sind optional.

Was CQRS ist – und was nicht

Weil sich rund um den Begriff so viele Halbwahrheiten sammeln, hier die klare Sortierung. CQRS ist:

  • die fachliche Trennung von Schreib- und Lesemodell,
  • ein Muster für ausgewählte Bounded Contexts,
  • kombinierbar mit Event Sourcing, wenn die Domäne es rechtfertigt,
  • von Anfang an mit einem einzigen Store praktizierbar.

CQRS ist nicht:

  • ein Framework oder eine Library,
  • ein Synonym für Event Sourcing,
  • eine Pflicht zu zwei Datenbanken,
  • ein systemweiter Architekturstandard, den man überall ausrollt.

An dieser zweiten Liste hängt der meiste Ärger, den ich in Reviews sehe.

Die drei teuersten Verwechslungen

Die erste ist der Versuch, CQRS überall einzusetzen. Es ist ein Minderheitsfall, kein Default. Fowler warnt ausdrücklich, dass CQRS für die meisten Systeme riskante Komplexität hinzufügt und man beim Einsatz sehr vorsichtig sein sollte. Die Trennung gehört in einen einzelnen Bounded Context, wo die Domäne sie rechtfertigt – nicht in jedes Modul, nicht in jede Entität. Wer CQRS systemweit ausrollt, zahlt überall die Kosten der Trennung und erntet den Nutzen nur an den wenigen Stellen, an denen die Modelle wirklich auseinanderdriften.

Die zweite ist, CQRS mit Event Sourcing zu verwechseln. Das ist die häufigste Verwechslung überhaupt, und ich verstehe, woher sie kommt – die beiden treten in Vorträgen und Beispielen fast immer gemeinsam auf. Aber sie sind unabhängige Muster. Event Sourcing ist eine Entscheidung über Persistenz: Statt den aktuellen Zustand zu speichern, speichert man die Folge der Events, die zu ihm führten. CQRS ist eine Entscheidung über Modelle: Schreiben und Lesen bekommen getrennte Modelle. Man kann jedes ohne das andere haben. Vaughn Vernon zeigt in seinen IDDD-Beispielen zwar Kontexte, die Event Sourcing und CQRS kombinieren – aber auch dort bleibt es eine bewusste Kombination zweier Muster, keine Definition, in der eines das andere enthält. Wer CQRS sagt und Event Sourcing meint, verhandelt in Diskussionen ständig aneinander vorbei.

Die dritte ist, zwei Datenbanken zu früh einzuführen. Getrennte Read- und Write-Stores sind eine optionale, spätere Optimierung – meist getrieben von asymmetrischer Last, wenn man Lese- und Schreibseite unabhängig skalieren will. Führt man sie zu früh ein, holt man sich zwei Kosten ins Haus, bevor irgendein Bedarf erwiesen ist: den Synchronisationsaufwand zwischen den Stores und, schwerwiegender, eventual consistency. Ab dem Moment, in dem der Read Store dem Write Store hinterherläuft, sieht der Nutzer möglicherweise Daten, die schon veraltet sind. Das ist ein fachliches Problem, kein technisches Detail – und man sollte es sich nur einhandeln, wenn man dafür einen konkreten Grund hat.

So sähe diese Ausbaustufe aus – die man bewusst erst dann geht, wenn die Last es verlangt:

flowchart LR
    CommandHandler --> WriteModel
    WriteModel --> WriteStore[(Write Store)]
    WriteStore -.->|projection<br/>eventual consistency| ReadStore[(Read Store)]
    QueryService --> ReadStore

Der gestrichelte Pfeil ist der Preis. Zwischen Write Store und Read Store liegt jetzt eine Projection, und mit ihr eine Verzögerung. Solange dieser Pfeil nicht nötig ist, sollte er nicht existieren.

Wann sich die Grenze wirklich lohnt

Nach all den Warnungen die konstruktive Seite: Es gibt Situationen, in denen CQRS die richtige Antwort ist. Ich sehe im Wesentlichen drei.

Die erste sind komplexe Domänen, in denen ein gemeinsames Modell keine Seite gut bedient. Wenn das Schreibmodell vor lauter Invarianten und Verhalten kaum noch abfragbar ist und die Leseseite sich mühsam durch dieses Modell quält, um simple Listen zu erzeugen, dann kämpfen beide gegen dieselbe Struktur. Die Trennung befreit beide.

Die zweite ist asymmetrische Last. Viele Systeme lesen um Größenordnungen häufiger, als sie schreiben. Getrennte Modelle – und, in der Ausbaustufe, getrennte Stores – erlauben es, die Leseseite unabhängig zu skalieren, ohne die Schreibseite anzufassen.

Die dritte sind aufgabenorientierte Oberflächen. Wenn die UI ohnehin in Absichten denkt – „Artikel deaktivieren", „Bestellung stornieren" – statt in CRUD-Formularen, dann liegt die Command-Seite von CQRS ohnehin schon in der Luft. Man muss sie nur noch benennen.

Diese Kriterien decken sich mit einer Haltung, die meine Architekturarbeit durchgehend trägt: Struktur folgt aus der Fachlichkeit, nicht aus dem Wunsch nach einem modernen Diagramm. CQRS ist kein Ziel. Es ist eine Antwort – und nur dort richtig, wo die Domäne die passende Frage stellt.

Fazit

Wer CQRS als Framework begreift, sucht an der falschen Stelle. Es gibt nichts zu installieren. Die einzige Entscheidung, die zählt, ist eine fachliche: Profitiert diese Domäne davon, dass Schreiben und Lesen unterschiedliche Modelle bekommen? Wenn ja, zieht man die Grenze – klein, mit einem einzigen Store, ohne Event Sourcing, in einem einzelnen Bounded Context. Wenn nein, lässt man es, und das ist der häufigere Fall.

Die getrennten Datenbanken, die Projektionen, die eventual consistency – all das sind spätere, optionale Ausbaustufen, die man geht, wenn die Last es verlangt, und nicht früher. CQRS mit diesen Ausbaustufen gleichzusetzen ist der Fehler, der das Muster teuer und angsteinflößend macht. In seiner Essenz ist es das Gegenteil: eine unspektakuläre, billige, fachlich motivierte Trennung. Zwei Fragen, zwei Modelle. Mehr braucht es nicht, um anzufangen.

Weiterführende Quellen

Kommentare