Kolumne: Olis bunte Welt der IT

Verantwortung teilen mit CQRS und Event Sourcing
Kommentare

Verantwortung teilen ist gut. Wer mag schon die ganze Verantwortung tragen, wenn die halbe es auch tut? Auch technisch macht das Teilen von Verantwortung und den damit zusammenhängenden Zuständigkeiten fast immer Sinn. Aber in der Praxis sind die notwendigen Schritte nicht immer so einfach.

Ich erinnere mich zum Beispiel lebhaft an die Einführung von XAML als Seitenbeschreibungssprache für grafische Bedienoberflächen. Besonders von Microsoft wurde damals eine Aufteilung in den Vordergrund gestellt, die jeder Programmierer zu schätzen wissen sollte: Die visuelle Erscheinung einer Anwendung sollte fortan einem Designer überlassen sein. Der Programmierer sollte sich nur noch um die technischen Hintergründe kümmern und einem Designer vertrauen, das Werk in eine ästhetisch angenehme Form zu bringen.

Nun fand ich persönlich diese Idee sofort richtig gut. Ich spare als Entwickler viel Zeit, wenn ich mich nicht um visuelle Fragen kümmern muss, und als Berater hatte ich auch die Hoffnung, in Zukunft wesentlich weniger grün und magenta gestylte Formulare zu Gesicht zu bekommen, in denen es Editoren für Primärschlüssel und elf Ebenen verschachtelter Gruppen gibt. Natürlich stand dem letztlich ein praktisches Problem im Wege: Die meisten Entwicklungsteams beschäftigten bisher keinen Designer.

Interessant war aber auch, welche Gegenwehr von Seiten der Entwickler zu beobachten war. Eigentlich gab es wesentlich wichtigere Pläne zur Verantwortungsteilung in XAML-basierten Anwendungen, zum Beispiel in Bezug auf die neuen mächtigen Features der Datenbindung und der Patterns, die sich somit anboten. Selbst heute noch basieren viele WPF-Anwendungen nicht auf MVVM oder vergleichbaren Konzepten, und auch Ressourcen werden oft nicht so genutzt, wie es wünschenswert wäre.

Der Wert der Erfahrung

Nun soll dies keine Tirade sein, sondern nur ein objektiver Blick auf die Schwierigkeiten, die wir alle haben, wenn ein Muster, auf das wir lange Zeit gesetzt haben, sich ändert. Sicher gibt es oft praktische Gründe, warum die Neuheiten sich nicht sofort für bestehende Anwendungen anbieten oder für neue Projekte gar zu umständlich erscheinen. Aber vor allem möchten wir gern an den Erfahrungen der Vergangenheit festhalten und ein neuer Blickwinkel erscheint zunächst oft bedrohlich.

Über diesen Sachverhalt habe ich kürzlich nachgedacht, als ich einige Arbeit im Bereich der Patterns CQRS und Event Sourcing getan habe. Beide Patterns werden, wie ich bei meiner Recherche festgestellt habe, oft als komplex, kompliziert und für viele Anwendungen überdimensioniert angesehen und beschrieben. Gerade mit den Beschreibungen ist es allerdings nicht so einfach, denn ich habe auch herausgefunden, dass die meisten Beschreibungen der Patterns unangenehm schwierig sind. Dabei ist die Sache eigentlich ganz einfach, und ich hoffe, mit meiner eigenen Beschreibung und meinen Einschätzungen dazu beitragen zu können, das Image dieser Patterns etwas zu verbessern.

Schreiben ist anders als Lesen

Die logische Grundlage für CQRS besteht in der Erkenntnis, dass das Schreiben von Daten oft technisch anders funktioniert als das Lesen. CQRS bedeutet Command/Query Responsibility Segregation. Das Schreiben von Daten findet sich in der Bezeichnung als Command wieder, da ein Kommando direkt einen schreibenden Datenzugriff abbilden kann oder ein solcher als Konsequenz des Kommandos stattfindet. Das Lesen hingegen wird mit dem Begriff Query beschrieben, also mit einer Abfrage von Informationen.

Um die Grundlagen der Idee zu verstehen, denken Sie an ein Objektmodell Ihrer Daten, wie etwa mit ORM modelliert. Da gibt es einen Typ, der gewisse Geschäftsdaten kapselt und Instanzen dieses Typs im Speicher erzeugt, wenn Daten geladen werden. Dann gibt es außerdem Fähigkeiten, die im ORM mittels eines Patterns wie Unit of Work Änderungen an den geladenen Instanzen aufzeichnen und bei Bedarf wieder zur Datenbank schicken.

Nun betrachten Sie einmal, wie sich solch eine Struktur in einer traditionellen Webanwendung verhält. Nehmen wir an, dass ein Anwender zunächst auf eine Website zugreift, um eine Übersicht vorhandener Daten anzusehen. Auf dem Server wird der Request für die Seite verarbeitet, Daten werden abgefragt und Objektinstanzen erzeugt. Mithilfe dieser Daten wird dann die Übersicht erzeugt und zurück zum Browser geschickt.

Nun nehmen wir weiterhin an, dass der Anwender einen der Datensätze bearbeitet und die Änderungen wieder zum Server gelangen. Zur Behandlung des neuen Requests muss nun also der fragliche Datensatz wiederum geladen und eine Objektinstanz erzeugt werden. Diese Instanz kann dann anhand der Modifikationsdetails vom Browser geändert werden, und das ORM-System sorgt dafür, dass diese Änderungen in die Datenbank geschrieben werden.

Weniger ist mehr

Technisch betrachtet funktioniert dieser Vorgang einwandfrei, hat aber leider einen wesentlichen Overhead. Zunächst werden beim Laden der Daten relativ komplexe Objekte instanziiert, deren Funktionalität zum Teil unnötig für den Vorgang ist. Ein größeres Problem in der Praxis ist allerdings die Struktur der Typen, denn diese sind gewöhnlich entweder für die Ablage in einer relationalen Datenbank optimiert (oder gar aus einer bestehenden relationalen Struktur generiert), oder für die Kapselung von Geschäftsdaten entlang der implementierten Geschäftslogik. Es ist wahrscheinlich, dass die Datenübersicht in meinem Beispiel manche der Details aus den abgefragten Objekten gar nicht nutzt, oder dass die Abfrage mehrerer Typen notwendig ist, um die Übersicht zu füllen.

Beim Schreiben ist der dargestellte Vorgang ebenfalls nicht optimal. Mindestens ein Objekt wird erzeugt, um die Änderungen aufzunehmen und in einer Unit of Work aufzuzeichnen – dabei liegen die Änderung bereits vom Client vor.

Nun kommt der erfahrene Programmierer natürlich schnell auf den Gedanken, das Schreiben der Daten zu optimieren, indem ein Datenbankzugriff direkt auf Basis der vom Browser empfangenen Änderungsinformationen durchgeführt wird. Je nach eingesetztem ORM-System gibt es sogar Hilfsmittel, um etwa diesen Zugriff unabhängig von der verwendeten Datenbank durchführen zu können. Dieser Ansatz zur Optimierung spiegelt die Kernidee von CQRS wider: Lesen und Schreiben von Daten sollten als separate Vorgänge mit eigenen Prioritäten und Anforderungen verstanden werden.

In einer Anwendung, die CQRS als Pattern für Datenzugriffe einsetzt, gilt es, alle geschäftlichen Vorgänge entlang der Trennlinie zwischen Lesen und Schreiben aufzuteilen. Dabei sind hier natürlich geschäftliche Vorgänge auf einer technischen Ebene gemeint, denn größere Vorgänge können sich durchaus aus Lese- und Schreibvorgängen zusammensetzen. Wo Daten gelesen werden, ergibt sich aus den Anforderungen des Vorgangs auf einfache Weise, welche Informationen benötigt werden. Beim Schreiben können die effektivsten Methoden eingesetzt werden, Änderungen und Neudaten zu persistieren.

Nichts ist schwarz/weiß

Natürlich ist es technisch möglich, für beide Seiten ein ORM-System einzusetzen, und das mag noch immer wichtige Vorteile wie etwa die Unabhängigkeit vom Datenbanksystem bieten. Gleichzeitig bietet die Aufteilung viele Ansatzpunkte für Optimierungen sowie die eingangs erwähnten generellen Vorteile der Verantwortungsteilung. Wie immer gilt: Je modularer eine Architektur ist, desto flexibler ist ihre Erweiterung und Pflege.

Es sei erwähnt, dass das beschriebene Beispiel, in dem ORM in Zusammenhang mit einer Webanwendung in ein gewisses Licht gestellt wurde, nicht in jedem anderen Anwendungskonzept nachvollziehbar ist. Wenn Sie etwa eine traditionelle monolithische Desktopanwendung bauen, in der geladene Daten womöglich langfristig im Speicher gehalten werden, stellen sich die Vorteile von ORM anders dar. Allerdings ist selbst in solchen Fällen die Koordination von Vorgängen oft einfacher, wenn Sie eine logische Trennung zwischen Command und Query anstreben, da Sie z. B. erzeugte Kommandos als Events verarbeiten können.

Ausgehend vom aktuellen Punkt meiner Beschreibung ist der Weg zum Event Sourcing nicht sehr beschwerlich. Stellen Sie sich vor, in Ihrem System wird jede Änderung von Daten mithilfe eines Kommandos beschrieben. Ein Kommando könnte etwa durch ein Objekt dargestellt werden, das einen Kommandotyp sowie notwendige Parameter enthält. Die Sequenz aller verarbeiteten Kommandos führt in Bezug auf die Datenhaltung von Punkt A zu Punkt B. Zu Beginn ist Ihre Datenbank leer. Nachdem ein Kommando ausgeführt wurde, dessen Konsequenz die Anlage eines neuen Datensatzes ist, enthält die Datenbank natürlich diesen Datensatz. Nach einigen Änderungskommandos stellt der Datensatz den aktuellen Zustand dar, der sich aus den Kommandos ergibt.

Ereignisse als Quelle

Die Idee von Event Sourcing besteht darin, in erster Linie die Kommandos aufzuzeichnen, ohne dass auch resultierende Datensätze angelegt oder modifiziert werden. Da der aktuelle Stand der Daten sich jederzeit aus den aufgezeichneten Kommandos berechnen lässt, ist nichts verloren. Im Gegenteil: Da die Kommandos selektiv verarbeitet werden können, lässt sich auch jeder Zwischenstand im Nachhinein wiederherstellen. Wenn in einem Event-Sourcing-System ein Kommando eingeht, wird es lediglich an das Ende der Aufzeichnung angehängt.

Es stellt sich natürlich die Frage, was beim Lesezugriff auf die Daten geschieht. Natürlich wäre es nicht sehr performant, bei jedem Lesezugriff den aktuellen Stand jedes relevanten Datensatzes zunächst aus den Kommandos herzuleiten. Tatsächlich mag es ein, dass ein Datensatz bereits im Speicher vorhanden ist, denn Event-Sourcing-Systeme haben gewöhnlich ein Abbild der von den Kommandos erzeugten Entitäten parat, um möglichst schnell weitere Kommandos verarbeiten zu können. Für die Abfragen gibt es allerdings zusätzlich das Konzept der Projektionen, die bei eingehenden Kommandos aktuell gehalten werden. Projektionen werden so definiert, dass Sie einen bestimmten Zweck im Rahmen der Geschäftslogik erfüllen, etwa „alle Kunden, deren Umsatz im aktuellen Jahr oberhalb von X liegt, absteigend sortiert nach Umsatz, mit diesen Feldern: …“

Hier noch einmal der Gesamtvorgang bis zu diesem Punkt, an dem eine neue Entität angelegt und geändert wird. Zunächst gibt es ein Kommando, das in der Erzeugung einer neuen Entität resultiert. Das Kommando wird aufgezeichnet und die Entität wird im Speicher erzeugt. Wenn es definierte Projektionen gibt, die sich auf diesen Typ von Entität beziehen, werden diese angepasst. Das passiert in derselben Weise bei einem Änderungskommando, und wenn eine Abfrage ausgeführt wird, kann sie sofort mit den vorbereiteten Daten der Projektion versorgt werden.

Datenbanken gibt es trotzdem

Letztlich werden auch in einer Anwendung, die Event Sourcing verwendet, aus verschiedenen Gründen Daten im aktuellen Zustand in Datenbanken abgelegt. Zum einen dient dies der Erzeugung sogenannter Snapshots, die von manchen Systemen in zeitlichen Abständen, oder basierend auf dem Durchsatz des Systems, erzeugt werden. Wenn das System neu gestartet werden muss, kann mithilfe eines Snapshots der aktuelle Stand schneller wiederhergestellt werden, da nur ein Teil der Kommandos „abgespielt“ werden muss.

Zum anderen ist es oft sinnvoll, sich für Lesevorgänge nicht allein auf Projektionen zu verlassen. Wenn etwa Datenabfragen dynamisch ausgeführt werden sollen, sodass die Kriterien und andere Details einer Abfrage nicht im Vorhinein bekannt sind, ist es nützlich, ein oder mehrere Read Models zu pflegen. Diese Read Models sind in Struktur und Umfang spezifisch auf bestimmte Gruppen von Abfragen abgestimmt, sodass die Abfragen einfach sein und die Resultate schnell geliefert werden können.

Zum Erzeugen der Read Models bedienen sich Programmierer meist eines Features, das ich bisher noch nicht erwähnt habe. Wenn Kommandos verarbeitet werden, lösen Event-Sourcing-Systeme gewöhnlich Domain Events aus, die über einen Bus auch an andere Dienste des Gesamtsystems verschickt werden. Damit kann ein separater und unabhängiger Dienst ein Read Model erzeugen und beliebige andere Reaktionen auf bestimmte Kommandos implementieren.

Sind nun CQRS und Event Sourcing geeignete Patterns für alle Anwendungen? Natürlich nicht. Ich empfehle aber, über die Konsequenzen der Verwendung beider Ansätze im Detail nachzudenken. Meine persönliche Einschätzung ist, dass CQRS selbst sehr oft dem logischen Ablauf entspricht, den geschäftliche Vorgänge generell haben, und dass die Aufteilung entlang der Trennung zwischen Lese- und Schreibvorgängen wesentliche Vorteile in der Wartbarkeit bietet. Demgegenüber steht für CQRS nur ein geringer Aufwand, der gefühlt nach einer Gewöhnungsphase ganz verschwindet.

Event Sourcing ist ein Pattern, das ich ebenfalls sehr zu schätzen weiß. Strukturell erscheint es zunächst aufwändiger als ein direkter Zugriff auf Daten, aber die Flexibilität im Gesamtsystem steigt gleichzeitig enorm und rechtfertigt etwas zusätzliche sorgfältige Architekturplanung. Event Sourcing ist sicherlich ein Pattern, das sich am einfachsten und effektivsten in einer Anwendung umsetzen lässt, die nach modernen Servicekonzepten aufgebaut ist.

Vorsicht bei der Konsistenz

Als Letztes möchte ich eine Warnung in Hinsicht auf das verwandte Thema Datenkonsistenz anbringen. Bisher habe ich das nicht erwähnt, aber beim Einsatz von CQRS wird oft eine Aufteilung der gespeicherten Daten in mehrere Datenbanken sinnvoll oder gar notwendig. CQRS ohne Event Sourcing kann mit Lese- und Schreibmodellen arbeiten. Dann müssen aber natürlich zusätzlich Elemente in die Architektur aufgenommen werden, die eine Synchronisation der Daten zwischen den Modellen gewährleisten. Mit Event Sourcing lässt sich dieses Problem lösen. In jedem Fall gibt es aber die Konsequenz, dass der Vorgang eines Updates in einem Read Model unabhängig ist von der Ausführung eines Kommandos, das das Update auslöst. Deshalb ist transaktionelle Konsistenz bei einer vollen Umsetzung aller besprochenen Ideen schwieriger umzusetzen, und man arbeitet stattdessen mit Eventual Consistency (letztliche Konsistenz).

Für Eventual Consistency gibt es Beispiele in moderner Software sowie im wirklichen Leben. Wenn Sie in einem Kaffeehaus einer internationalen Kette in der Schlange stehen, fragt Sie womöglich bereits ein Barista, was Sie bestellen möchten. Ihre Bestellung wird mit lautem Geschrei an die nächste Stelle weitergeleitet, wo umgehend die Vorbereitung des Getränks beginnt. Nun erreichen Sie die Kasse und bezahlen für den Kaffee, den Sie noch gar nicht gesehen haben. Den bekommen Sie hoffentlich kurze Zeit später am Ende einer anderen Schlange, und hoffentlich schmeckt er Ihnen auch, andernfalls wird er einfach nochmal neu gebraut. Das ist Eventual Consistency – am Ende gehen Sie hoffentlich mit dem Getränk, das Sie haben wollten und für das Sie gezahlt haben.

Ähnliches passiert beim Einkaufen online. Sie legen ein Produkt in den Warenkorb und wenn Sie, womöglich lange später, dafür zahlen wollen, stellt das System eventuell fest, dass das Produkt gar nicht mehr verfügbar ist. Es kommt sogar vor, dass nach schon abgeschlossener Bestellung eine E-Mail folgt, in der Sie informiert werden, dass das Produkt doch nicht – oder erst später – geliefert werden kann. Auch in diesem Fall kommt es hoffentlich, aber nicht immer, irgendwann zur „letztlichen Konsistenz“.

Viele große Firmen setzen solche Systeme ein, da sich damit die Verarbeitung von Gesamtvorgängen wesentlich effizienter gestalten lässt. Allerdings ist ebenfalls offensichtlich, dass es manchmal zu Problemen kommt, und dann bekommen Sie einen neuen Kaffee oder einen Gutschein als Entschuldigung für die Lieferschwierigkeiten. Dieser Ausgleich ist ein wichtiges Element im Umgang mit Eventual Consistency.

Fazit

Ich empfehle sehr, sich die Konzepte von CQRS und Event Sourcing für neue Anwendungen im Detail anzueignen. Es gibt Anwendungen, für die der Einsatz der beiden Konzepte nicht empfehlenswert ist, aber ich würde nach den Gründen gegen eine solche Entscheidung fragen. Behalten Sie auf jeden Fall die Problematik der Datenkonsistenz im Kopf, und stellen Sie sicher, dass vorher die Teile Ihrer Geschäftslogik identifiziert werden, die in jedem Fall mit transaktioneller Konsistenz arbeiten müssen. Bitte hängen Sie nicht zu sehr an alten Konzepten – im Großen und Ganzen lernen wir doch alle ständig dazu!

Windows Developer

Windows DeveloperDieser Artikel ist im Windows Developer erschienen. Windows Developer informiert umfassend und herstellerneutral über neue Trends und Möglichkeiten der Software- und Systementwicklung rund um Microsoft-Technologien.

Natürlich können Sie den Windows Developer über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist der Windows Developer ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -