Einführung in CQRS mit NestJS
Michał Cywiński
03. März 2023・17 Min. Lesezeit
Inhaltsverzeichnis
UserService vor dem Refactoring
Probleme mit dem bestehenden UserService
CreateUserCommand
SendActivationEmailEventHandler
GetActiveUsersQuery
NestJS‑Controller
Definieren wir das Problem
Seit vielen Jahren werden die meisten Anwendungen mit einer klassischen N‑Schichten‑Architektur gebaut (siehe unten). Dennoch führt dieser Ansatz selbst dann sehr häufig zu übermäßig komplexen Codebasen, wenn Entwicklungsteams an Best Practices wie Design Patterns, die Anwendung der SOLID‑Prinzipien und Unit‑Tests für Services festhalten. Solche Codebasen nennen wir oft eine "Big Ball of Mud".
An diesem Punkt fragst du dich vielleicht: „Warum entsteht diese Big Ball of Mud, obwohl mein Team und ich nach State‑of‑the‑Art‑Praktiken arbeiten? Und warum passiert das in den meisten Projekten, an denen ich beteiligt bin?“ Auch wenn wir anfangs die genannten Prinzipien befolgen, werden die Schichten im Laufe der Zeit immer komplexer. Am Ende referenzieren viele Services einander, und Tests werden mühsam.
Hier kamen Microservices ins Spiel. Alle waren begeistert, weil es so aussah, als gäbe es endlich das Allheilmittel für die genannten Probleme. Nach einiger Zeit stellten jedoch viele Teams fest, dass dieselben Probleme wieder auftraten – nur schwerer zu lösen: „eine verteilte Big Ball of Mud“. Und nicht nur dieselben Probleme: Nun musste man sich auch mit Kommunikations‑ und Datensynchronisations‑Themen herumschlagen – ganz zu schweigen von mehreren Deployment‑Einheiten, die zu managen waren. Ein ganzes Orchester an Problemen, das harmonisiert werden will – und eines, dem sich Entwicklungsteams bis heute stellen.
Wie entkommen wir also dieser Komplexitäts‑Hölle? Gibt es eine Möglichkeit für eine schön aufgeräumte Codebasis, die man gerne mit Unit‑Tests abdeckt?
Ja. Ich stelle dir das CQRS‑Pattern vor. Es löst nicht alle Probleme, aber in jedem Team, für das ich gearbeitet habe, hat CQRS viele Kopfschmerzen beseitigt.
CQRS – Definition
CQRS ist ein Akronym für Command Query Responsibility Segregation. Aber was bedeutet das konkret? Das CQRS‑Pattern geht davon aus, dass ein Entwickler drei Haupttypen von Request‑Klassen nutzt, um Business‑Logik auszulösen und zu orchestrieren:
Commands, die den Zustand der Anwendungsdaten verändern;
Queries, die ausschließlich zum Abrufen von Anwendungsdaten dienen, ohne den Anwendungszustand zu ändern;
Events, die während der Ausführung von Business‑Logik ausgelöst werden können, z. B. um nach einem Command zusätzliche Verarbeitung anzustoßen.
Jeder der oben genannten Request‑Typen hat einen zugehörigen Handler, der die eigentliche Business‑Logik ausführt. Stell dir einen Handler wie eine einzelne Service‑Methode vor. Request‑Klassen wie Commands tragen lediglich die Eingaben (Payload) für einen bestimmten Handler.
Wichtig ist außerdem: Commands und Queries haben jeweils genau einen zugehörigen Handler. Events können mehrere Handler haben. Wir können z. B. ein Event sowohl per E‑Mail als auch per SMS verarbeiten – zwei unterschiedliche Handler, die auf dasselbe Event reagieren.
Jeder Request‑Typ wird über den entsprechenden Bus veröffentlicht: Command Bus, Query Bus oder Event Bus. Diese Buses kennen die registrierten Handler (in der Regel über einen Dependency‑Injection‑Container) und wissen, welcher Handler mit der bereitgestellten Request‑Instanz auszuführen ist.
Aber was heißt das in der Praxis? Genug Theorie – schauen wir es uns an einem Beispiel an.
CQRS am Beispiel
Im folgenden Beispiel betrachten wir CQRS, indem wir einen Teil eines Application‑Service in Commands, Queries, Events und deren zugehörige Handler refaktorisieren.
UserService vor dem Refactoring
Unten steht eine einfache UserService‑Klasse, die für das Anlegen eines Users und das Abrufen aktiver Users verantwortlich ist.
Die Erstellungslogik legt einen User mit gehashtem Passwort an und verschickt eine Aktivierungs‑E‑Mail. Nach dem Anlegen geben wir die ID des neu erstellten Accounts zurück.
Die Abruflogik filtert User‑Accounts auf aktive Einträge und mappt sie anschließend auf DTOs.
Probleme mit dem bestehenden UserService
Bevor wir refaktorisieren, sollten wir uns ansehen, wo die Probleme solcher Service‑Klassen bzw. dieses klassischen Service‑Ansatzes liegen:
Was hat das Anlegen eines Users mit dem Abrufen aktiver Users zu tun? Abgesehen davon, dass beide denselben Repository für den Datenzugriff nutzen … eigentlich nichts.
Wenn wir das Abrufen aktiver Users unit‑testen wollen, müssen wir einen NotificationService mocken oder injizieren, der mit getActiveUsers gar nichts zu tun hat.
Services werden im Projektverlauf immer unübersichtlicher. Mit diesem Ansatz endet man zwangsläufig bei Spaghetti‑Code.
Uns wurde jahrelang gesagt, wir sollten keine Business‑Logik in Controller packen. In der Praxis verschieben wir das Chaos jedoch nur von den Controllern in die Services. Das Ergebnis: Dasselbe Chaos – nur eben in Services. Das bringt uns in Sachen Wartbarkeit unserer Codebasis keinen Vorteil.
Mit diesem Wissen ist es Zeit für ein Refactoring. Dafür nutzen wir das ausgezeichnete @nestjs/cqrs‑Package für NestJS – mit großartiger Dokumentation.
CreateUserCommand
Das Einführen unseres ersten Commands samt Handler ist so einfach wie das Deklarieren zweier zugehöriger Klassen: CreateUserCommand und CreateUserCommandHandler (letzterer muss mit @CommandHandler dekoriert werden und das ICommandHandler‑Interface implementieren).
Im Command speichern wir die Informationen, die zum Anlegen eines User‑Accounts und zum Ändern des Anwendungszustands nötig sind: E‑Mail‑Adresse und Passwort. Unser Handler enthält die aus dem ursprünglichen UserService extrahierte Logik.
Bemerkenswert ist, dass wir eine neue Abhängigkeit injizieren: den EventBus aus @nestjs/cqrs. Er dient dazu, ein Event zu raisen, das anzeigt, dass ein User‑Account erstellt wurde. Event‑Handler (weiter unten erklärt) können solche Events unabhängig konsumieren und darauf reagieren – in unserem Fall durch das Versenden einer Aktivierungs‑E‑Mail mit einem Secret‑Token.
Das wirklich Schöne: Unit‑Tests werden im Vergleich zum vorherigen Ansatz super einfach. Wir müssen nur prüfen, ob der User mit den korrekten Eigenschaften in der Datenbank gespeichert wurde und ob ein Event geraised wurde (über einen EventBus‑Mock überprüfbar).
Es spielt also keine Rolle, wie viele Handler auf das veröffentlichte Event reagieren – sie werden ohnehin unabhängig getestet. Das verschafft uns mehr Freiheit, weil wir nicht von Services wie dem NotificationService abhängig sind, der im ursprünglichen UserService injiziert wurde. Wir können damit die gesamte durch die Registrierung ausgelöste Logik von der eigentlichen Erstellung der User‑Entität entkoppeln. Ziemlich cool, oder?
SendActivationEmailEventHandler
Der Name ist Programm. Das ist unser Event‑Handler, der auf das im CreateUserCommandHandler geraiste UserCreatedEvent reagiert und anhand der im Event bereitgestellten Payload eine Aktivierungs‑E‑Mail verschickt.
Wie bei Command‑Handlern müssen wir ihn mit dem @EventsHandler‑Decorator dekorieren und das IEventHandler‑Interface aus @nestjs/cqrs implementieren.
Hier wird der Notification‑Service injiziert. Beachte: Wir können nun das Versenden der Aktivierungs‑E‑Mail unit‑testen, ohne überhaupt einen User‑Account anzulegen. Unser Code ist deutlich modularer.
Außerdem können wir uns problemlos einen weiteren Event‑Handler vorstellen, der unabhängig auf dasselbe Event reagiert – z. B. durch das Versenden einer SMS. Auch dieser Handler lässt sich unabhängig von anderen Event‑Handlern und vom auslösenden Command‑Handler testen. Je ereignisorientierter unsere Anwendung wird, desto mehr profitieren wir von dieser Architektur.
GetActiveUsersQuery
Zu guter Letzt führen wir die refaktorierte GetActiveUsersQuery und den GetActiveUsersQueryHandler ein, dekoriert mit @QueryHandler und mit Implementierung des IQueryHandler‑Interfaces.
Die Query‑Klasse trägt alle notwendigen Informationen (Payload), um die im zugehörigen Handler definierte Logik auszuführen. In unserem Fall ist das der isActive‑Filter. Nach dem Filtern werden die Ergebnisse – wie im ursprünglichen UserService – auf DTOs gemappt.
NestJS‑Controller
Mit allen Queries und Commands an Ort und Stelle können wir QueryBus und CommandBus in unseren API‑Controller injizieren. Die Handler werden automatisch aufgelöst und aufgerufen. Wir werden alle Abhängigkeiten (Services) los – außer den Buses. Aufgeräumt, oder?
Einführung von CQRS in bestehende Codebases
Man könnte sagen: „Okay, cool. Schönes Pattern, aber ich arbeite an einer Legacy‑Codebasis. Ich kann das bei uns gerade nicht einführen.“ Das ist kein Argument. Du kannst das Pattern auch in eine bestehende Codebasis einführen. Manchmal leicht, manchmal mit etwas zusätzlichem Aufwand. Ich habe das in Enterprise‑Codebasen getan – und es hat sich immer gelohnt.
Du musst das Pattern nicht auf einmal einführen. Tatsächlich spielt CQRS seine Stärken aus, weil du es inkrementell einführen kannst, ohne dein Projekt zu brechen. Neue Funktionalitäten lassen sich meist sofort als Command‑ und Query‑Handler implementieren. Ja, ein sofortiger Gewinn.
Klar, gelegentlich musst du komplexe Services injizieren (die man im Idealfall gar nicht injizieren möchte) – aber du kannst sie mocken und damit Unit‑Tests (und dir selbst) das Leben leichter machen.
Bei bestehenden Services kannst du mit dem Refactoring beginnen, sobald du wegen deines aktuellen Jira/Trello/Dein‑Lieblings‑Issue‑Tracker‑Tickets eine Service‑Methode anfasst: Extrahiere die Logik zunächst unverändert in einen neuen Handler, damit du neue Features nicht mit Aufräumarbeiten vermischst. Wie immer: Füge Unit‑Tests hinzu, falls es noch keine gab.
Erstelle einen Pull Request, stelle sicher, dass alle den refaktorierten Code verstehen, und merge ihn in deinen Development‑Branch. Jetzt ist Zeit für die eigentliche Aufgabe. Du solltest neue Features nun mit wenig Aufwand ergänzen können, da du nur noch auf der CQRS‑Handler‑Ebene arbeitest.
Genau das wollten wir erreichen: supersimple Klassen, die genau eine Sache tun – und nur die Abhängigkeiten nutzen, die sie wirklich benötigen.
Fazit
CQRS erfordert etwas zusätzlichen Code für die Payloads unserer Handler, macht es aber extrem einfach, viele kleine, wartbare Klassen zu haben, die sich leicht unit‑testen lassen.
Würde ich es für jedes Projekt empfehlen? Wahrscheinlich nicht. Wenn du nur ein paar einfache CRUDs brauchst und schnell liefern willst, fährst du mit klassischen Service‑Klassen möglicherweise besser. Spätestens wenn sich der Projektumfang ändert oder die Komplexität steigt, solltest du ein Refactoring in Betracht ziehen.
In meinem Entwickler‑Werkzeugkasten ist das CQRS‑Pattern für mittelgroße und größere Projekte immer gesetzt. Es wird sich auszahlen – versprochen.
Digital Transformation Strategy for Siemens Finance
Cloud-based platform for Siemens Financial Services in Poland


Das könnte Ihnen auch gefallen...

So finden Sie 2023 die beste Node.js-Entwicklungsagentur: Ein umfassender Leitfaden
Node.js hat sich als Schlüsseltechnologie für die Entwicklung robuster, skalierbarer Webanwendungen etabliert. Vor diesem Hintergrund ist die Rolle einer Node.js-Entwicklungsagentur wichtiger denn je. Dieser Artikel zeigt Ihnen, wie Sie die besten Node.js-Entwicklungsunternehmen für Ihre spezifischen Anforderungen auswählen.
Marek Pałys
02. Feb. 2023・6 Min. Lesezeit

Tutorial: Ruby und Ruby on Rails installieren und RubyGems verwenden
Entdecken Sie eine Schritt-für-Schritt-Anleitung zur Installation und Nutzung von Ruby on Rails für die effiziente Entwicklung von Webanwendungen. Erfahren Sie, wie Sie Ruby einrichten, verschiedene Versionen verwalten, mit RubyGems und Bundler arbeiten und ein neues Ruby on Rails-Projekt erstellen. Starten Sie mit diesem umfassenden Leitfaden in die Entwicklung mit Ruby on Rails.
Jan Grela
20. März 2020・6 Min. Lesezeit

Tech-Stack 2020: GraphQL, Apollo Server und React.js
Seit dem Jahr 2000 gelten RESTful-Prinzipien als Branchenstandard für den Aufbau von Web-APIs – doch GraphQL bietet eine effizientere Alternative. Dieser Artikel beleuchtet die Vorteile von GraphQL gegenüber REST, seine Popularität und zeigt, wie man mit Apollo GraphQL einen Server und eine Client-Anwendung entwickelt.
Wojciech Cichoradzki
12. Mai 2020・7 Min. Lesezeit
Bereit, Ihr Know-how mit KI zu zentralisieren?
Beginnen Sie ein neues Kapitel im Wissensmanagement – wo der KI-Assistent zum zentralen Pfeiler Ihrer digitalen Support-Erfahrung wird.
Kostenlose Beratung buchenArbeiten Sie mit einem Team, dem erstklassige Unternehmen vertrauen.
Wir entwickeln, was als Nächstes kommt.
Dienste




