CQRS als Grundlage für moderne, flexible und skalierbare Anwendungsarchitektur

(Bild: erzeugt mit KI durch iX)
Man hört oft, komplexe Systeme basierten auf CQRS. Doch was genau ist das eigentlich, CQRS? Was verbirgt sich hinter dem Akronym, und wie kann man es nutzen?
Vor zwei Wochen habe ich einen Blogpost als Einführung in Event-Sourcing [1] geschrieben. Das Thema ist bei vielen von Ihnen auf sehr großes Interesse gestoßen, es gab sehr viel Feedback dazu, und natürlich gab es auch eine ganze Reihe von Fragen. Im heutigen Blogpost greife ich viele dieser Fragen auf und beantworte sie. Sie bekommen aber vor allem auch eine weitere Einführung in ein mit Event-Sourcing zusammenhängendes Konzept: Heute geht es nämlich auch um ein Architektur-Pattern namens CQRS. Was das ist, wofür es gut ist, wie es funktioniert, was die Anwendungsfälle sind, wie Sie es mit Event Sourcing kombinieren können, und so weiter – all das erfahren Sie heute.
CQRS mit oder ohne Event Sourcing?
Fangen wir einmal, ganz kurz, mit einer Klarstellung an: Vielleicht haben Sie von CQRS schon gehört, und Sie würden nun gerne wissen, was genau es damit auf sich hat, aber vielleicht haben Sie den Blogpost zu Event Sourcing vor zwei Wochen nicht gelesen. Da stellt sich nun die Frage: Können Sie den heutigen Post trotzdem "einfach so" lesen, oder sollten Sie sich zuerst mit dem Thema Event Sourcing beschäftigen?
Nun, dafür ist ganz wichtig: CQRS und Event Sourcing sind zwei Konzepte, die unabhängig voneinander existieren. Das heißt, Sie können CQRS durchaus umsetzen, ohne Event Sourcing nutzen zu müssen, und Sie können Event Sourcing auch umsetzen, ohne CQRS nutzen zu müssen, und insofern könnten Sie diesen Blogpost auch als in sich geschlossenes Thema ansehen.
Aber: Die beiden Konzepte ergänzen einander ausgesprochen gut. Insofern werden Sie sie in der Praxis eher selten isoliert voneinander vorfinden, sondern in der Regel gemeinsam. Deshalb baue ich in diesem heutigen Blogpost auf dem auf, was ich vor zwei Wochen geschrieben habe. Sollten Sie jenen Blogpost [2] also noch nicht gesehen haben, dann würde ich Ihnen empfehlen, das jetzt nachzuholen. Alternativ können Sie ihn sich auch als Video [3] ansehen.
Was ist CQRS?
Doch was ist CQRS überhaupt? Das Akronym steht für "Command Query Responsibility Segregation", also für die Trennung der Verantwortlichkeiten für Commands und Queries.
Tatsächlich ist das sehr viel einfacher, als es zunächst klingt. Die Kernidee von CQRS ist, dass es für den Zugriff auf Anwendungen nur zwei Arten von Interaktionsmustern gibt: Entweder führt die Anwenderin oder der Anwender eine Aktion aus, mit der Absicht, im System etwas zu verändern, oder sie oder er möchte etwas wissen. Mehr Möglichkeiten gibt es nicht. Das heißt, entweder teilen Sie einer Software mit, dass sie dies oder jenes für Sie erledigen soll, dann geben Sie ihr quasi einen Befehl (das ist dann ein sogenannter "Command") – oder Sie fragen die Software nach einer Information, Sie stellen also eine Frage oder führen eine Abfrage durch (und das ist dann eine sogenannte "Query").
Empfohlener redaktioneller Inhalt
Mit Ihrer Zustimmung wird hier ein externes YouTube-Video (Google Ireland Limited) geladen.
Ich bin damit einverstanden, dass mir externe Inhalte angezeigt werden. Damit können personenbezogene Daten an Drittplattformen (Google Ireland Limited) übermittelt werden. Mehr dazu in unserer Datenschutzerklärung [4].
Das eine, also ein Command, zieht üblicherweise eine Veränderung und damit einen Schreibvorgang nach sich, das andere, also eine Query, greift hingegen nur lesend zu. Man könnte also auch sagen: Es gibt schreibende und lesende Zugriffe. Das CQRS-Entwurfsmuster besagt nun, dass Sie Ihre Anwendung zweiteilen sollten: in einen Teil, der sich um Schreibvorgänge kümmert, und in einen anderen Teil, der sich um Lesevorgänge kümmert. Sie sollen also, und genau das war meine ursprüngliche Aussage, die Verantwortlichkeit für das Schreiben von der für das Lesen trennen.
Worüber dieses Design-Pattern jedoch nichts aussagt, ist, auf welche Art Sie diese Trennung bewerkstelligen sollen. Es ist also erst einmal völlig offen, ob das einfach nur bedeutet, dass Sie gedanklich zwischen schreibenden und lesenden API-Routen trennen, oder ob Sie zwei getrennte APIs bauen, oder ob das zwei getrennte Services sind, oder sonst etwas. Das ist, wie es im Englischen so schön heißt, "up to interpretation". Darauf kommen wir nachher jedoch näher zu sprechen.
Wozu das Ganze?
Die erst einmal viel spannendere Frage ist: Warum sollte man das überhaupt machen? Also, warum gibt es diese Empfehlung in Form des CQRS-Patterns überhaupt?
Nun, dahinter steckt eine sehr einfache Überlegung, und sie hat erst einmal sehr wenig mit APIs zu tun, sondern viel mehr mit Datenbanken: Wenn wir über klassische Architektur sprechen, haben wir normalerweise eine UI, eine irgendwie geartete API und am Ende eine Datenbank. Obwohl die Datenbank dabei buchstäblich nicht im Mittelpunkt der Entwicklung steht, beschäftigen wir uns in der Softwareentwicklung üblicherweise sehr ausführlich mit ihr, und häufig hat das in der Datenbank gewählte Datenmodell deutliche Auswirkungen darauf, wie die Geschäftslogik in der API gebaut wird. Das heißt, eine essenzielle Frage lautet: Wie sieht ein gutes Datenmodell aus?
Die akademische Welt hat darauf eine passende Antwort: In der Datenbanktheorie gibt es nämlich verschiedene Normalformen. Das kann man sich so ein bisschen vorstellen wie einen Satz von Regeln, wie man Datenmodelle gestalten sollte. Konkret gibt es davon fünf verschiedene Varianten, und die fünfte Normalform entspricht ein wenig dem heiligen Gral: Wenn man sie umsetzt, dann bedeutet das nichts anderes, als dass alles sehr sauber und ordentlich strukturiert ist, dass keinerlei Information dupliziert wird, dass es keine Redundanzen in den Daten gibt, und so weiter. Das ist deshalb erstrebenswert, weil sich dann Konsistenz- und Integritätsregeln sehr leicht umsetzen lassen. Die fünfte Normalform ist so gesehen also ein Traum, wenn es darum geht, Daten zu schreiben.
Wo Licht ist, ist auch Schatten
Ein wenig unglücklich ist nur, dass man Daten in der Regel auch wieder lesen muss. Das ist in der fünften Normalform natürlich möglich, nur muss man hierfür meist sehr komplexe Abfragen ausführen, und man braucht dann, um zum Beispiel etwas Simples wie die Stammdaten einer Anwenderin auszulesen, auf einmal 27 Joins. Das heißt, Lesen ist durchaus machbar, aber es ist völlig ineffizient und langsam. Das ist natürlich sehr unpraktisch und im Alltag wenig nützlich. Also, kurz gesagt: Die fünfte Normalform ist gut zum Schreiben geeignet, aber eine Katastrophe zum Lesen.
Das andere Extrem wäre die erste Normalform: Hier wird einfach für jede View, die es in der UI gibt, eine passende Tabelle angelegt, und die Daten werden überall dorthin verteilt, wo man sie gerade braucht. Das heißt, die Daten werden komplett denormalisiert, was das Lesen rasend schnell macht: Denn mehr als ein SELECT * FROM xy
ist dann in der Regel nicht erforderlich, schließlich gibt es ja per Definition für jede View eine passende Tabelle. Insofern ist die erste Normalform ein Traum, wenn es um das Lesen von Daten geht.
Allerdings ist das Schreiben in diesem Fall eher schwierig, denn man muss dieselbe Information mehrfach ablegen, man muss sie mehrfach pflegen, und es ist deshalb sehr kompliziert, Konsistenz und Integrität zu wahren. Man könnte also festhalten, dass die erste Normalform genau das Gegenteil der fünften ist: Die erste ist gut zum Lesen, aber schlecht zum Schreiben.
Die Normalformen in der Praxis
Wie sieht es nun in der Realität aus? In der Regel nimmt man weder die fünfte noch die erste Normalform, sondern man wählt die Dritte: Das ist der übliche Kompromiss zwischen den beiden Welten, und ich persönlich würde sagen, dass man damit letztlich das Modell nimmt, das weder zum Schreiben noch zum Lesen besonders gut geeignet ist. Man hat nämlich weder einfache Abfragen mit hoher Performance noch die gewünschte Konsistenz und Integrität. Das heißt, mit anderen Worten: Man bekommt das Schlechteste der beiden Welten.
Und was hat das Ganze nun mit CQRS zu tun? Nun, die Aussage war ja, dass man das Schreiben vom Lesen trennen sollte. Und genau das kann man hier nun wunderbar auf Datenbankebene machen: Anstatt zu versuchen, die ganz unterschiedlichen Anforderungen des Schreibens und des Lesens mit einem einzigen Datenmodell zu erfüllen (was, wie gesagt, nicht besonders gut funktioniert), nimmt man schlichtweg zwei Datenmodelle – nämlich eines, das auf das Schreiben optimiert ist, und eines, das auf das Lesen optimiert ist. Dann hat man beim Schreiben die gewünschte Integrität und Konsistenz, und beim Lesen hat man effiziente und performante Abfragen. Der einzige Haken ist dabei nur, dass man natürlich die Änderungen, die im Schreib-Modell erfolgen, irgendwie in das Lese-Modell überführen muss, aber dazu kommen wir nachher noch.
Ein Beispiel: Die Stadtbibliothek
Bevor wir nun weiter in die Details gehen, möchte ich erst einmal ein kleines Beispiel vorstellen, das das Ganze ein wenig veranschaulicht: In dem Blogpost vor zwei Wochen hatte ich als Fachdomäne eine Bibliothek angesprochen, also zum Beispiel eine Stadtbibliothek, in der man sich Bücher ausleihen kann. Ich hatte außerdem geschrieben, dass der Schwerpunkt beim Event Sourcing weniger auf den Substantiven liegt, sondern vielmehr auf den Verben – also mit anderen Worten: auf den Prozessen.
Und welche Prozesse gibt es in einer Bibliothek? Richtig: Man kann Bücher ausleihen, man kann die Ausleihe verlängern, man kann Bücher zurückgeben, man kann sie (aus Versehen oder absichtlich) beschädigen, und so weiter.
All diese Aktionen resultieren, wenn wir uns das als Software vorstellen, in Commands: Als Anwenderin oder Anwender möchte ich zum Beispiel ein Buch ausleihen, also suche ich es im System heraus, prüfe, ob es verfügbar ist, und wenn ja, tippe ich einen Button an, auf dem steht: "Buch ausleihen". Genau das ist mein Command: "Dieses Buch jetzt ausleihen!". Das klingt schon wie ein Befehl. Das System führt dann in Folge Geschäftslogik aus, befolgt dabei die hinterlegten Business-Regeln, prüft, ob ich das Buch ausleihen darf, und entscheidet dann, was passiert. Und damit hat sich dann der Zustand des Systems verändert, was wiederum genau die Definition eines Commands war: ein Schreibvorgang, der den Zustand des Systems ändert.
Wir haben bereits vor zwei Wochen darüber gesprochen, dass sich zum Erfassen dieser Veränderungen Event-Sourcing ganz wunderbar anbietet. Und die Datenbank, in der ich all diese Events im Lauf der Zeit sammle, also ein Append-Only-Log, ist unser Schreib-Modell.
Was ist mit Fragen?
Nun möchte die Bibliothek aber vielleicht gerne wissen, welche Bücher gerade ausgeliehen sind. Oder: Welche Bücher wurden im vergangenen Jahr am häufigsten verlängert? Oder: Gibt es Bücher, die in den vergangenen sechs Monaten gar nicht ausgeliehen wurden? Wie viel Prozent der ausgeliehenen Bücher wurden beschädigt zurückgegeben? Und so weiter.
Diese Liste an Fragen kann man beliebig verlängern.
Nun ist aber der Punkt: All diese Fragen lassen sich zwar auf Basis der Events beantworten, nur leider ist das nicht besonders effizient. Denn wenn ich zum Beispiel wissen will, welche Bücher derzeit alle verliehen sind, dann kann ich theoretisch alle Events von Anfang an durchgehen und eine Liste führen, in der ich jedes Mal, wenn ein Buch ausgeliehen wurde, einen Eintrag mache, und immer dann, wenn ein Buch zurückgegeben wurde, diesen Eintrag wieder streiche. Und wenn ich das von Anfang bis Ende durchziehe, weiß ich am Schluss, welche Bücher aktuell ausgeliehen sind.
Das Gute daran ist also: Ich bekomme diese Information aus meinen Events heraus. Das nicht ganz so Gute daran ist: Es ist ziemlich aufwendig.
All diese Fragen werden gemäß CQRS als Queries angesehen, weil ich etwas wissen möchte und dadurch nicht den Zustand des Systems verändere. Um diese Fragen effizient und performant beantworten zu können, wäre es viel einfacher, wenn ich ein dediziertes Lesemodell hätte. Stellen wir uns also ganz kurz vor, wie das am leichtesten wäre: Die einfachste Möglichkeit wäre eine Tabelle, in der alle aktuell ausgeliehenen Bücher aufgeführt sind. Wenn ich dann nämlich wissen will, welche Bücher gerade verliehen sind, wäre das wirklich nur ein SELECT * FROM xy
. Oder, wenn ich wissen möchte, ob ein bestimmtes Buch gerade ausgeliehen ist, könnte ich ebenfalls auf diese Tabelle zugreifen und sagen: SELECT * FROM xy WHERE titel = abc
. Entweder bekomme ich einen Treffer, und dann ist das Buch aktuell verliehen, oder eben nicht. Hier wäre also tatsächlich ein denormalisiertes Datenmodell die perfekte Lösung.
Lesemodelle aufbauen
Die spannende Frage ist nun: Wo bekomme ich eine solche Tabelle her? Denn die einzigen Daten, die wir aktuell speichern, sind die Events. Die Antwort auf diese Frage ist tatsächlich sehr einfach: Wir bauen die Liste, so wie eben beschrieben, einfach nebenher auf. Also immer, wenn ein Event vom Typ "Buch wurde ausgeliehen" gespeichert wird, speichern wir nicht nur dieses Event, sondern wir tragen das Buch auf Basis der in diesem Event enthaltenen Informationen auch in unsere Liste der aktuell ausgeliehenen Bücher ein.
Immer, wenn ein Event vom Typ "Buch wurde zurückgegeben" gespeichert wird, entfernen wir das Buch wieder von der Liste der ausgeliehenen Bücher. Und immer, wenn ein Event vom Typ "Buch wurde verlängert" gespeichert wird, machen wir mit unserer Tabelle der ausgeliehenen Bücher gar nichts, denn das Verlängern eines Buches ändert nichts daran, dass es bereits verliehen ist.
Sollte nun jemand kommen und uns fragen, welche Bücher aktuell ausgeliehen sind, dann können wir diese Frage ganz einfach beantworten, denn wir haben die Antwort quasi schon vorbereitet.
Und das Beste daran ist, dass wir nicht von Anfang der Entwicklung an wissen müssen, dass uns irgendwann einmal jemand fragen wird, welche Bücher aktuell gerade ausgeliehen sind: Wir können diese Lesemodelle nämlich auch nachträglich noch aufbauen, indem wir alle bereits gespeicherten Events abspulen. Wir können ein Lesemodell also auch im Nachhinein noch anpassen und es einfach neu aufbauen lassen. Wir können auch weitere Lesemodelle ergänzen, oder auch diejenigen, die wir nicht mehr brauchen, einfach entfernen.
Das heißt, wir haben zum Lesen kein statisches Schema mehr, sondern wir können uns gezielt für die relevanten Fragen passende Antwortmodelle zurechtlegen, damit wir sie im Bedarfsfall ad-hoc griffbereit haben. Das beschleunigt das Lesen enorm.
Ein Lesemodell pro Use Case
Es wird aber noch besser: Denn nicht jeder Lesevorgang hat dieselben technischen Anforderungen. Für die Liste der gerade ausgeliehenen Bücher eignet sich zum Beispiel eine relationale oder auch eine NoSQL-Datenbank hervorragend. Wenn ich aber zum Beispiel zusätzlich noch eine Volltextsuche über alle Bücher anbieten will, dann müsste ich, immer wenn das Event "Buch wurde neu in den Bestand aufgenommen" gespeichert wird, das Buch indexieren und diese Informationen an beliebiger Stelle ablegen.
Dafür ist aber weder eine relationale noch eine NoSQL-Datenbank à la MongoDB sonderlich gut geeignet, sondern dafür würde sich vielleicht Elasticsearch empfehlen. Dem Prozess, der auf das Event reagiert und daraus ableitet, wie die Lesetabellen zu aktualisieren sind, ist es aber völlig gleichgültig, ob er das für eine, zwei oder mehr Tabellen erledigt. Außerdem ist ihm auch egal, ob diese Tabellen in derselben Datenbank liegen oder ob wir mit verschiedenen Datenbanken sprechen. Mit anderen Worten: Ich kann mir ein Lesemodell, das auf einen bestimmten Use Case abzielt, nicht nur passgenau für diesen aufbauen, sondern ich kann das sogar mit der am besten geeigneten Technologie umsetzen, ohne vorher wissen zu müssen, dass diese Anforderung irgendwann einmal auftauchen wird. Damit wird das Ganze schon sehr flexibel.
Doch man kann es sogar noch weiter treiben: Da sich die Lesemodelle jederzeit aus den Events wieder rekonstruieren lassen, müssen sie theoretisch nicht einmal persistiert werden. Man könnte, zumindest, sofern sie nicht zu groß werden, die Lesemodelle auch einfach im RAM halten. Das ist übrigens gar nicht so abwegig, wie es vielleicht zunächst klingt. Zum einen deshalb nicht, weil Lesezugriffe damit natürlich noch einmal einen enormen Performanceschub erhalten, denn technisch gesehen kann der Zugriff kaum noch schneller erfolgen.
Zum anderen, weil inzwischen einige Systeme auf dem Markt sind, die genau so arbeiten, wie zum Beispiel Memgraph [5] oder DuckDB [6] (zu dem ich vor einigen Monaten auch einen Blogpost geschrieben hatte: "Ente gut, alles gut? [7]").
Das Data-Mesh lässt grüßen
Das Ganze ist übrigens auch, wenn Sie in Richtung eines Data-Meshs gehen möchten, eine unglaublich gute Ausgangsbasis: Bei einem Data-Mesh geht es ja letztlich darum, dass ein Team, das eine bestimmte fachliche Verantwortung hat, anderen Teams seine Daten passend zur Verfügung stellen kann (als sogenanntes "Data Product"). Und genau dieses "passend zur Verfügung stellen" ist häufig gar nicht so einfach, denn wie oft entspricht schon das interne Datenmodell genau dem, woran jemand anderes interessiert ist?
Das Tolle an Event Sourcing und CQRS ist nun, dass Sie für ein anderes Team einfach ein passendes Lesemodell aufbauen können. Das hat keinerlei Einfluss auf das interne Datenmodell oder auf die Datenmodelle für andere Teams. Und Sie können ein solches Datenmodell auch jederzeit wieder ändern oder entfernen, ohne befürchten zu müssen, dass Sie irgendjemandem etwas kaputtmachen, weil Sie ja einfach für jeden ein eigenes Lesemodell bereitstellen können. Insofern kann ich Ihnen nur raten, wenn das Thema Data-Mesh für Sie relevant ist, sich Event Sourcing und CQRS unbedingt genauer anzusehen.
Und übrigens, nur um es ganz kurz erwähnt zu haben: Auch unterschiedliche Zugriffsrechte lassen sich damit sehr elegant abbilden. Sie bieten für verschiedene Rollen einfach unterschiedliche Lesemodelle an, und die Rolle mit höheren Rechten erhält ein Lesemodell, das mehr Daten enthält, wohingegen die Rolle mit niedrigeren Rechten ein Lesemodell mit weniger Daten erhält. Auch hier können Sie sich das Leben deutlich vereinfachen, im Vergleich dazu, alles in einer einzigen Tabelle mit zig verschiedenen und entsprechend komplexen Abfragen steuern zu müssen.
Wie sieht es mit der Last aus?
Nun fragen Sie sich vielleicht:
"Alles schön und gut, aber das bedeutet ja, dass die Last auf meiner Leseseite unter Umständen ganz schön ansteigt. Habe ich da nicht das Problem, dass meine Lese-API mittelfristig überlastet wird?"
Die einfache Antwort lautet: Nein. Denn Sie können nicht nur verschiedene Lesemodelle parallel zueinander betreiben, sondern Sie können auch dasselbe Lesemodell auf mehrere Datenbank- und Server-Instanzen verteilen. Schließlich gilt auch hier wieder: Dem Mechanismus, der auf Events reagiert und dementsprechend die Lesetabellen aktualisiert, ist es egal, ob das unterschiedliche Lesetabellen sind oder ob es mehrere Kopien derselben Tabelle auf unterschiedlichen Servern gibt.
Das heißt, Sie können die Leseseite praktisch ohne Overhead beliebig skalieren, indem Sie einfach parallelisierte Kopien des Lesemodells vorhalten, und das sogar individuell pro Lesemodell, je nachdem, auf welches Lesemodell viel und auf welches eher wenig zugegriffen wird. Das heißt, CQRS ermöglicht es Ihnen, genau dort und nur dort zu skalieren, wo die Last am höchsten ist.
Nur eventuell konsistent?
Wie auch schon bei Event Sourcing möchte ich Ihnen natürlich auch bei CQRS die Herausforderungen nicht verschweigen. Denn es ist nicht so, dass es keine gäbe: Auch CQRS ist keine magische Wunderlösung, die alle Ihre Probleme "einfach so" löst.
Das Hauptproblem ist, dass die erforderliche Synchronisation zwischen der Schreib- und der Leseseite Zeit benötigt. Ich habe Ihnen diesen Mechanismus so beschrieben, dass er auf gerade gespeicherte Events reagiert und dann die Lesemodelle anpasst. Nun ist klar, dass wenn wir hier über ein verteiltes System mit verschiedenen Datenbanken sprechen, zwischen dem Speichern des Events und dem Aktualisieren des Lesemodells ein wenig Zeit vergeht. Das wird im Normalfall nicht allzu viel sein (meist handelt es sich um ein paar Millisekunden), aber es bedeutet eben, dass die Leseseite immer ein kleines Stück hinterherhinkt.
Es ist dabei jedoch nicht so, dass die Leseseite nicht konsistent wäre: Das ist sie schon, nur ist sie das eben nicht sofort, sondern sie benötigt einen kurzen Augenblick. Das nennt man, in Abgrenzung zur "strong consistency" dann "eventual consistency". Doch da muss man aufpassen, denn das wird im Deutschen gerne als "eventuell konsistent" übersetzt – was jedoch falsch ist. Tatsächlich heißt "eventually consistent" nämlich so viel wie letztlich oder schlussendlich konsistent. Die Frage, die sich also stellt, lautet: Wie gravierend ist dieser kleine zeitliche Versatz in der Praxis?
Fachliche nicht zu technischen Problemen machen
Die Antwort lautet: "Es kommt darauf an."
Tatsächlich ist das nämlich keine technische Frage, sondern eine fachliche: Wie hoch ist die Wahrscheinlichkeit, dass dieser geringe zeitliche Versatz zu einem Problem führt, wie sähe dieses Problem aus, wie groß ist das Risiko im Falle des Falles, und was könnten wir dann unternehmen?
Je nachdem, wie Sie diese Fragen beantworten, ergibt sich, ob Eventual Consistency ein Problem darstellt oder nicht. Um es noch einmal zu betonen, weil das so gerne missverstanden wird: Das ist keine technische Frage. Das ist nichts, was Entwicklerinnen und Entwickler entscheiden könnten. Das ist etwas, was der Fachbereich entscheiden muss. Und der Punkt dabei ist natürlich, dass es durchaus Alternativen zu Eventual Consistency gibt (zum Beispiel durch eine einzige große verteilte Transaktion), nur hätte das dann andere Nachteile. Und falls Sie schon einmal mit verteilten Transaktionen in verteilten Systemen zu tun hatten, dann wissen Sie, dass das alles ist, nur kein Vergnügen.
Insofern: Es geht nicht darum, zu sagen, Eventual Consistency sei grundsätzlich gut oder schlecht, oder Strong Consistency sei grundsätzlich gut oder schlecht, sondern es geht darum, herauszufinden, welches Konsistenzmodell für den konkreten fachlichen Use Case besser geeignet ist.
Ein Geldautomat geht offline
Da sagen viele:
"Nein, also auf Strong Consistency können wir unter gar keinen Umständen verzichten!"
Und ja, natürlich gibt es Szenarien, in denen das so ist: Das ist aus meiner Erfahrung vor allem dann der Fall, wenn die nationale Sicherheit betroffen sein könnte oder wenn es um den Schutz von Leib und Leben geht. In 99,9 Prozent aller Business-Anwendungen ist das meiner Erfahrung nach aber völlig nebensächlich, da reicht Eventual Consistency in aller Regel mehr als aus.
Um hier das gängige Lehrbuch-Beispiel anzuführen: Ein Geldautomat, der die Netzwerkverbindung verliert, wird Ihnen trotzdem noch für eine gewisse Zeit Bargeld auszahlen. Denn die meisten Menschen heben ohnehin keine besonders hohen Beträge ab, und die meisten Menschen machen dies nur, wenn sie wissen, dass genug Geld auf ihrem Konto ist. Das heißt, die Wahrscheinlichkeit und das Risiko, dass die Bank Geld herausgibt, das Ihnen nicht zusteht, ist äußerst gering.
Selbst wenn das passiert, dann holt die Bank es sich eben mit Zinsen und Zinseszinsen zurück. Das heißt, aus dem netzwerktechnischen Ausfall zieht die Bank im Zweifelsfall sogar noch einen Vorteil. Dieses Vorgehen ist aus Geschäftssicht für die Bank weitaus besser, als die vermeintlich naheliegende technische Lösung zu wählen und den Geldautomaten offline zu schalten – denn dann würde am nächsten Tag möglicherweise auf Seite 1 der Boulevardzeitung stehen:
"Skandal! Multimilliardär stand vor dem Geldautomaten und konnte keine 50 Euro abheben! Ist das die neue Service-Wüste?"
Und das möchte garantiert niemand.
Ist das nicht alles viel zu kompliziert?
Ich glaube, damit haben Sie einen ganz guten Überblick darüber, was hinter CQRS steckt und auch, warum CQRS und Event Sourcing so gut zusammenpassen: Sie ergänzen sich einfach sehr, sehr gut, weil das Append-Only-Prinzip von Event Sourcing ein sehr einfaches Modell zum Schreiben von Daten ist, aus dem sich dann äußerst flexibel beliebige Lesemodelle generieren lassen.
Unterm Strich wirkt das Ganze vielleicht anfangs ein wenig einschüchternd. Das kann ich gut nachvollziehen, denn mir ging es vor mehr als zehn Jahren, als ich anfing, mich mit diesen Themen zu beschäftigen, nicht anders. Aber eigentlich sind diese Themen gar nicht so übermäßig kompliziert, sondern sie sind nur sehr anders als das, was die meisten von uns gewohnt sind. Und das dauert einfach, weil man sich komplett umgewöhnen muss und vieles, was man bislang über Softwareentwicklung gedacht und geglaubt hat, quasi entlernen muss.
Wenn man das aber einmal geschafft hat, wirkt diese "neue Welt" sehr viel intuitiver und sinnvoller als die klassische, herkömmliche Entwicklung. Viele, die sich daran gewöhnt haben, fragen sich hinterher, wie sie jemals der Meinung sein konnten, klassisch zu entwickeln, sei eine gute Idee gewesen. Mir ging das so, und vielen unserer Kunden, die wir bei the native web [8] bei der Einführung von Event Sourcing und CQRS beraten und unterstützt haben, ebenfalls.
Also lassen Sie sich davon bitte nicht abschrecken. Es wird Ihnen am Anfang schwierig erscheinen, weil Ihnen die Erfahrung fehlt, doch nach einer Weile werden Sie zurückblicken und denken:
"Wow, das war eine der besten Entscheidungen in meinem Leben als Entwicklerin oder Entwickler, mich auf diese Themen einzulassen."
Wenn Sie mehr wissen möchten, und Sie diesen Blogpost (und den vergangenen zu Event Sourcing) spannend fanden, dann habe ich eine gute Nachricht für Sie: Ich werde in den kommenden Wochen und Monaten noch einige weitere Blogposts zu diesen Themen schreiben, und dabei nach und nach auch mehr ins Detail und mehr in die Praxis gehen. Und wir werden auf unserem YouTube-Kanal [9] über kurz oder lang auch einen Livestream dazu machen.
In diesem Sinne: Bleiben Sie gespannt!
(mai [10])
URL dieses Artikels:
https://www.heise.de/-10275526
Links in diesem Artikel:
[1] https://www.heise.de/blog/Event-Sourcing-Die-bessere-Art-zu-entwickeln-10258295.html
[2] https://www.heise.de/blog/Event-Sourcing-Die-bessere-Art-zu-entwickeln-10258295.html
[3] https://www.youtube.com/watch?v=ss9wnixCGRY
[4] https://www.heise.de/Datenschutzerklaerung-der-Heise-Medien-GmbH-Co-KG-4860.html
[5] https://memgraph.com/
[6] https://duckdb.org/
[7] https://www.heise.de/blog/Ente-gut-alles-gut-DuckDB-ist-eine-besondere-Datenbank-9753854.html
[8] https://www.thenativeweb.io/
[9] https://www.youtube.com/@thenativeweb
[10] mailto:mai@heise.de
Copyright © 2025 Heise Medien