Was man über CQRS wissen sollte

the next big thing Golo Roden  –  83 Kommentare

CQRS ist ein Architekturmuster, das häufig im Zusammenhang mit Domain-driven Design (DDD) und Event-Sourcing genannt wird. Den Begriff prägte Greg Young 2010, inhaltlich basiert CQRS auf dem Entwurfsmuster CQS von Bertrand Meyer. Was steckt dahinter?

Das CQS-Entwurfsmuster (Command Query Separation) schlägt eine Zuordnung von Methoden eines Objekts zu zwei Kategorien vor: Entweder verändert eine Methode den internen Zustand des Objekts, gibt dann aber nichts zurück. Eine solche Methode heißt "Command". Oder eine Methode gibt Informationen zurück, verändert dann aber den internen Zustand nicht. Eine solche Methode heißt "Query".

Gemäß CQS sollte eine Methode niemals beides zugleich sein. Betrachtet man beispielsweise die gängige Datenstruktur des Stacks, ist die push-Funktion ein Command, top hingegen ist eine Query. Die Funktion pop verletzt schließlich das CQS-Muster, da sie den internen Zustand des Stacks verändert und zugleich Informationen zurückgibt.

Im Grunde geht es bei CQS also um das Trennen von Schreib- und Lese-Vorgängen im Hinblick auf ein einzelnes Objekt. Besonders relevant wird das beispielsweise dann, wenn Code parallelisiert ausgeführt werden soll: Queries lassen sich nämlich mangels Seiteneffekten problemlos parallelisieren, Commands hingegen nicht.

Greg Young hat aufbauend auf dem CQS-Muster im Jahr 2010 das CQRS-Architekturmuster (Command Query Responsibility Segregation) geprägt. Es trennt ebenfalls das Schreiben vom Lesen, allerdings in Bezug auf die API. Es schlägt daher separate APIs vor, von denen sich eine den Command-Routen widmet, die den Zustand der Anwendung verändern, und die andere den Query-Routen, die Informationen über den Zustand der Anwendung zurückgeben.

Technisch lässt sich das in HTTP beispielsweise derart implementieren, dass man die Command-API ausschließlich mit POST-Routen implementiert, die Query-API hingegen ausschließlich mit GET-Routen. Die eigentliche Semantik wird dabei in die URL verlagert. Um beispielsweise eine Bestellung aufzugeben, wäre dann ein POST-Request an die Route "/submit-order" erforderlich. Das steht im Gegensatz zu REST, das als Verben lediglich die HTTP-Verben zulässt, wobei fachliche Semantik bereits auf API-Ebene verloren geht.

Spinnt man den Gedanken von CQRS weiter, erscheint auch eine Trennung der hinter der API liegenden Datenbank in zwei Datenbanken sinnvoll. Eine sollte auf das Schreiben, die andere auf das Lesen optimiert sein – beispielsweise, indem die eine stark normalisiert, die andere hingegen denormalisiert wird. Auf dem Weg lassen sich gute Integrität und Konsistenz beim Schreiben gewährleisten, zugleich aber auch hohe Effizienz und Performance beim Lesen erreichen.

Was ist CQRS?

APIs mit CQRS entwickeln

Will man eine API passend zu CQRS implementieren, genügt es nicht, die Routen per POST und GET zu trennen. Man muss sich außerdem überlegen, wie sich erreichen lässt, dass ein Command nichts zurückgibt – das ist in HTTP nämlich gar nicht möglich, da zumindest ein Statuscode an den Client geliefert werden muss.

Im einfachsten Fall gibt man daher stets einen Statuscode 200 zurück, der besagt, dass das Command den Server erreicht hat. Alternative Statuscodes können lediglich 400, 401 oder 500 sein – es werden aber keinerlei fachliche Informationen zurückgegeben. Der eigentliche Response-Body ist aber in der Regel leer. Anders sieht es hingegen beim Request-Body aus, der verwendet wird, um die Daten und Metadaten des Commands zu übertragen.

Bei der Query-API verhält es sich ähnlich: Hier beschreibt der Pfad der URL die gewünschte Abfrage, die Parameter werden in diesem Fall – da es sich um einen GET-Request handelt – aber mit Hilfe des Querystrings übertragen. Da die Queries auf die zum Lesen optimierte denormalisierte Datenbank zugreifen, können die Queries schnell und effizient ausgeführt werden.

Problematisch ist aber, dass ein Client ohne regelmäßiges Pullen der Query-Routen nicht erfährt, ob ein Command bereits verarbeitet und welches Ergebnis dabei erzielt wurde. Daher empfiehlt sich der Einsatz einer dritten API, der Events-API, die per Push-Benachrichtigung über Websockets, HTTP-Streaming oder einen ähnlichen Mechanismus über Ereignisse informiert.

Wer GraphQL kennt und sich bei der Beschreibung der Commands, der Query- und der Events-API an die Konzepte Mutation, Query und Subscription erinnert fühlt, ist auf genau dem richtigen Weg: GraphQL eignet sich optimal, um CQRS-basierte APIs umzusetzen.

APIs mit CQRS entwickeln

CQRS, DDD und Event-Sourcing

Wie eingangs erwähnt, wird CQRS häufig in Verbindung mit Domain-driven Design (DDD) und Event-Sourcing genannt. Obwohl die drei Konzepte unabhängig voneinander sind, ergänzen sie einander gut.

Ein Command, das an die Command-API einer CQRS-basierten Anwendung geschickt wird, lässt sich nämlich auch im DDD-Sinn als Command für ein Aggregate interpretieren. Das Aggregate produziert dann in Folge ein oder mehrere Domain-Events, die mit Event-Sourcing in einem Event-Store gespeichert und für einen späteren Replay des Aggregates verwendet werden können.

Außerdem werden die dabei erzeugten Domain-Events auch an die Events-API weitergeleitet, die sie wiederum an die verschiedenen verbundenen Clients ausliefert, die so quasi Echtzeit-Updates über fachliche Vorgänge innerhalb der Anwendung erhalten.

Außerdem werden die Domain-Events auch an die Lese-Seite der Anwendung weitergeleitet, um dort die (vorberechneten) Views zu aktualisieren. Dazu wird eine sogenannte Projektion eingesetzt, die entscheidet, welche Relevanz ein fachliches Domain-Event für welche View besitzt und dann die betroffenen Views mit Hilfe von CRUD-Anweisungen entsprechend anpasst.

Der Event-Store stellt in diesem Szenario die Single Source of Truth dar, mit deren Hilfe sich beliebige Views auch im Nachhinein noch aufbauen lassen. Allerdings ist dabei zu beachten, dass das Schreiben in die Views zeitlich vom Schreiben in den Event-Store entkoppelt wird, weshalb man sich als Entwicklerin oder Entwickler mit dem CAP-Theorem und Eventual-Consistency vertraut machen sollte.

CQRS, DDD und Event-Sourcing

CAP-Theorem und Eventual-Consistency

Das CAP-Theorem beschreibt im Prinzip ein Dreieck, dessen Ecken für "Consistency", "Availability" und "Partition Tolerance" stehen. Eine verteilte Anwendung verhält sich konsistent (Consistency), wenn ein Schreibzugriff, bevor er dem Client bestätigt wird, zunächst an alle übrigen Knoten der Anwendung repliziert wird, so dass alle Knoten des verteilten Systems stets eine einheitliche Antwort liefern können.

Der Aspekt der Verfügbarkeit (Availability) beschreibt hingegen, dass ein verteiltes System jederzeit auf lesende und schreibende Anfragen reagieren kann – es also niemals zu Wartezeiten oder abgelehnten Anfragen auf Grund des Zustands des Systems kommt. Die Ausfallsicherheit (Partition Tolerance) fordert schließlich, dass eine verteilte Anwendung auch dann weiterhin ausgeführt werden kann, wenn einzelne Knoten ausfallen oder die Netzwerkverbindungen zwischen ihnen abreißen.

Das CAP-Theorem besagt nun, dass von diesen drei Aspekten stets nur zwei möglich sind. Das bedeutet insbesondere, dass ein stets konsistentes und verfügbares System (CA) nur dann möglich ist, wenn man jegliche Ausfälle der Hardware ausschließen kann, was praktisch unmöglich sein dürfte. Daher wird man in der Praxis nur die Wahl zwischen CP und AP haben, also der Frage, ob ein verteiltes System im Falle eines Server- oder Netzwerkausfalls auf die Verfügbarkeit oder die Konsistenz verzichten soll.

Der Verzicht auf die Konsistenz klingt zunächst gefährlich, allerdings handelt es sich hierbei nicht um "keine Konsistenz", sondern eher um eine Art "verzögerte Konsistenz". Schließlich wird die Konsistenz wieder hergestellt, sobald die ausgefallenen Server wieder erreichbar sind. Genau das bezeichnet der Begriff "Eventual-Consistency", der im Deutschen am ehesten mit "letztlich konsistent" oder "schlussendlich konsistent" gleichgesetzt werden kann.

Solange man nicht gerade Anwendungen für in hohem Maße sicherheitskritische Bereiche (öffentliche Infrastruktur, medizinische Systeme …) entwickelt, dürfte Eventual-Consistency kein Problem darstellen. Der Verzicht auf die stets garantierte starke Konsistenz führt dann nämlich zu einer weitaus besser erreichbaren und reaktiven Anwendung, was gerade im Web- und Cloud-Umfeld ein großer (Geschäfts-)Vorteil sein kann. Zudem ist zu bedenken, dass auch die Realität in den wenigsten Fällen echte Konsistenz aufweist.

CAP-Theorem und Eventual-Consistency

Warum CQRS?

Bleibt zuletzt die Frage, warum man sich überhaupt mit CQRS beschäftigen sollte. Ein naheliegender Grund ist, dass es eine offensichtliche Ergänzung zu etlichen anderen Konzepten wie DDD, Event-Sourcing und GraphQL darstellt. Zudem zielt CQRS durch das Trennen von Schreiben und Lesen von Vornherein auf verteilte Architekturen ab, was es für den Einsatz in service-basierten Systemen prädestiniert, die im Web oder der Cloud ausgeführt werden.

Damit bietet CQRS auch alle Vorteile einer service-basierten Architektur, beispielsweise die individuelle Skalier-, Wart- und Testbarkeit der einzelnen Dienste. Auch der Betrieb verschiedener Versionen einer Geschäftslogik ist mit CQRS ein Leichtes, zudem können die Zugriffsrechte der einzelnen Services gezielt eingeschränkt werden, was der Sicherheit des Gesamtsystems wiederum zu Gute kommt.

Der Nachteil liegt allerdings in einer von Haus aus aufwändigeren und komplexeren Architektur verglichen mit einem klassischen Client-Server-System – es ist aber zu bedenken, dass CQRS auch etliche Vorteile bietet, die allerdings ihren Preis haben.

Eine der größten Stärken von CQRS besteht in der Möglichkeit, gerade im Verbund mit DDD und Event-Sourcing, den technischen vom fachlichen Code zu trennen. Daher lässt sich die Geschäftslogik anpassen, ohne am technischen Unterbau etwas ändern zu müssen. Das gleiche gilt, was noch viel wichtiger ist, auch umgekehrt, was der langfristigen Stabilität und dem grundlegenden Vertrauen in die Geschäftslogik zuträglich ist.

Auf Grund der erwähnten Komplexität ist es ratsam, zu überlegen, ob man die eigene Anwendung auf einem passenden Framework aufbaut, das CQRS und Event-Sourcing bereits implementiert, so dass man sich als Entwicklerin oder Entwickler primär um das Entwerfen und Schreiben des fachlichen Codes konzentrieren kann. Derartige Frameworks gibt es für zahlreiche Technologien, Sprachen und Plattformen – unter anderem mit wolkenkit auch für JavaScript, TypeScript und Node.js.

Warum CQRS?

Fazit

CQRS ist ein spannender Ansatz für verteilte Architektur, der insbesondere im Zusammenspiel mit DDD und Event-Sourcing seine Stärken ausspielen kann. Die Komplexität übersteigt zwar die einer klassischen Client-Server-Architektur, allerdings erhält man auch eine weitaus besser skalierbare Anwendung, die zudem die grundlegende Fachlichkeit durchgängig besser abbilden kann.