- Published on
CQRS mit Domain Events
- Authors
- Name
- Ismail Bay
CQRS
CQRS steht für Command Query Responsibility Segregation und, vereinfacht gesagt, trennt die Applikation in zwei Kategorien:
- Schreibend:
Änderungen an Daten werden perCommand
s undDomain Event
s durchgeführt. - Lesend:
Für Lesezugriffe wird eine denormalisierte Form der Daten verfügbar gemacht.
Der lesende Teil der Applikation kann somit optimiert, skaliert und leichter angepasst werden.
Die gängige Form der CQRS ist die ereignisgesteuerte Variante mit einem Message-Broker, über den die Teile miteinander kommunizieren:
Vorteile
- Klare Trennung nach Kontext
- Entkopplung der Verantwortlichkeiten
- leichte Skalierbarkeit
- Refactoring des lesenden Teils viel leichter durchführbar
Nachteile
- sehr hohe Komplexität
- Datenkonsistenz nicht jederzeit gegeben
Die Implementierung von CQRS ist nicht schwierig, birgt aber einige Risiken:
- Datenintegrität über Services hinweg kann leichter verletzt werden
- Datenkonsistenz ist nur schlussendlich gegeben (eventual consistency)
- Deployments und Wartungen sind komplex
- Debugging und Tracing nicht trivial
Ohne ein gutes Systemdesign läuft man Gefahr, eine verteilte, monolithische Applikation zu entwickeln, die mehr Nachteile als Vorteile bringt.
Um diese Komplexität abzufangen, kann man eventuell auf ein etabliertes „Opinionated" Framework wie Axon setzen und sich auf die Implementierung der Geschäftslogik konzentrieren.
Domain Events und Fakten
Domain Events sind fachliche Ereignisse, die in der Domäne eines Systems eintreten und für Fachexperten relevant sind. Sie repräsentieren etwas, das in der Vergangenheit geschehen ist und den Zustand des Systems verändert hat.1 2
Domain Events haben einige wichtige Merkmale:
- hohe fachliche Semantik, ausgedrückt in der Sprache der Fachexperten, z.B. "Kostenvoranschlag hochgeladen"
- Unveränderlichkeit (Immutability), Beschreibung einer abgeschlossenen Tatsache
- Zeitliche Relevanz: wann ist dieses Ereignis eingetreten?
data class CostEstimateUploadedEvent(val id: UUID, val occuredOn: Instant, ...)
class CostEstimateUploadedEventHandler {
fun handleEvent(event: CostEstimateUploadedEvent) {
// Kostenvorschlag eventuell mit anderen Daten vermengen und read-optimiert speichern
log.info("received new cost estimate: ${event.id}")
}
}
Die zeitliche Relevanz der Events wird hier durch die Verwendung von Instant
festgehalten. Insbesondere in geografisch verteilten Systemen sollte die Verwendung von LocalDate
bzw. LocalDateTime
vermieden werden, da sie keine Zeitzonen beinhalten und somit keinen eindeutigen Punkt auf der globalen Zeitachse repräsentieren können.
Im obigen Beispiel wird das Event verwendet, um den Leseteil der Applikation zu aktualisieren.
Die entstandenen Events können aber auch von anderen Services konsumiert werden, solange sie dort eine Relevanz haben und der "Producer" dieser Events sie frei verfügbar macht.
Events als Teil der Wahrheit
Üblicherweise haben viele Events alleinstehend keine große Bedeutung. Sie präsentieren zwar Fakten in einem verteilten System, hängen jedoch von Daten in anderen Services ab bzw. sollen Aktionen in anderen Services auslösen.
Ein Kostenvoranschlag ohne eine Anfrage ergibt kaum Sinn. Ohne eine Preisanfrage würden alle Angebote ins Leere gehen. Um diese Zusammenhänge im System zu verwalten und eine transaktionale Einheit zu bieten, kommt das sogenannte Saga Pattern ins Spiel.
Mehr dazu im nächsten Artikel.