Fakten und Reaktionen mit Domain Events modellieren

Symbolbild zum Artikel: Fakten und Reaktionen mit Domain Events modellieren
Deine Reaktion:

Du bestellst online ein Buch. Klick auf „Kaufen“ startet eine Kette: Bestellung speichern, Bestätigungsmail versenden, Lager benachrichtigen, Statistik aktualisieren. Läuft alles glatt, fällt nichts auf. Doch wenn die Mail nicht rausgeht oder das Lagersystem überlastet ist, steht die Bestellung infrage – obwohl das Geschäftsereignis „Bestellung aufgegeben“ bereits aufgezeichnet wurde.

Der Autor des ursprünglichen Artikels zeigt an einem Workflow, wie aus einer einfachen Transaktion ein undurchsichtiger Block zusammenhängender Aktionen wird. Zunächst nur zwei Schritte: Bestellung speichern, Mail senden. Dann kommen Fulfillment-Benachrichtigung und Analyse hinzu. Jeder neue Schritt wird an die bestehende Liste angehängt. Problem: Die Geschäftstatsache – „Eine Bestellung wurde aufgegeben“ – gerät zwischen den technischen Details in den Hintergrund. Der Kunde wird von externen Systemen abhängig. Schlägt die Mail-Zustellung fehl, kann die gesamte Operation scheitern, obwohl die Bestellung im System sicher ist. Das führt zu doppelten Bestellungen oder unvollständigen Prozessen. Jeder Schritt braucht eigene Fehlerbehandlung, Wiederholungslogik und Toleranz. Wird die Bestellung an mehreren Stellen im Code aufgegeben, muss jeder Aufrufer dieselbe Kette nachbilden – ein Wartungsproblem.

Die Lösung: eine Trennung von Fakten und Reaktionen. Ein Domain Event beschreibt eine bereits eingetretene Tatsache in der Sprache des Fachbereichs. Nicht: „SendeBestätigungsMail“ (Befehl), sondern: „BestellungAufgegeben“ (Fakt). Die Vergangenheitsform zeigt, dass das Ereignis nicht rückgängig gemacht werden kann. Ein Handler kann „BestellungAufgegeben“ nicht ablehnen; er entscheidet nur, wie er reagiert. Ein Event ist unveränderlich. Ändert sich die Bestellung später, ist das ein neuer Fakt, der ein neues Event verdient.

Ein Domain Event trägt Informationen, die nötig sind, um den Fakt zu verstehen: meist die ID der Entität, relevante Werte zum Zeitpunkt des Ereignisses und einen Zeitstempel. Die genaue Zusammensetzung ist eine Designentscheidung. Manche Handler reichen mit einer ID aus, um Daten aus einer Read-Projection zu holen; andere brauchen einen vollständigen Schnappschuss. Der Autor rät, Events schlank zu halten und nur das Nötigste mitzugeben. Die innere Repräsentation des Aggregats sollte man nicht aus Bequemlichkeit preisgeben.

Damit wird der Unterschied zum Command deutlich. Ein Command drückt eine Absicht aus: „BestellungAufgeben“ fordert das Modell auf, etwas zu tun – und kann abgelehnt werden. Ein Domain Event zeichnet das Ergebnis auf: Akzeptiert das Modell den Command, wird „BestellungAufgegeben“ ausgelöst. Das Event entsteht im Arbeitsspeicher, während die Transaktion läuft. Erst nach erfolgreichem Abschluss wird das Event an die interessierten Handler verteilt.

Wo wird ein Domain Event erzeugt? Dort, wo die Entscheidung fällt. In einem reichen Domänenmodell ist das das Aggregat, das die relevanten Regeln durchsetzt. Die Sequenz: Geschäftsregeln prüfen, Zustand ändern, Event aufzeichnen. Das Aggregat hält das Event so lange bei sich, bis die Unit of Work es einsammelt. Kein Code außerhalb des Aggregats darf „BestellungAufgegeben“ konstruieren. Der Anwendungsdienst bittet das Modell, die Arbeit zu tun, und persistiert das Ergebnis. Das löst ein Problem des ursprünglichen Transaktionsskripts: Egal ob die Bestellung aus dem Shop, einem Admin-Tool oder einem Datenimport kommt – jeder Aufruf von order.place() zeichnet dasselbe Event auf. Neue Reaktionen wie Analyse oder Fulfillment lassen sich später hinzufügen, ohne jeden Aufrufer ändern zu müssen.

Die Reaktionen selbst leben in Event-Handlern. Jeder Handler hat eine Verantwortung: „Wenn dies passiert ist, tue jenes.“ Der Handler für die Bestätigungsmail nimmt das Event entgegen und versendet die Mail. Ein anderer Handler kümmert sich um die Fulfillment-Benachrichtigung. Handler wissen nichts voneinander und können hinzugefügt werden, ohne das Modell zu ändern, das das Event ausgelöst hat. Die geordnete Liste zusammenhangloser Arbeit wird zu einem Satz unabhängiger Aktionen, die jeweils eigene Fehlerbehandlung und Wiederholungslogik haben können.

Wer ruft die Handler auf? Das Aggregat selbst nicht – das ist Infrastrukturaufgabe. Die Unit of Work ist ein natürlicher Ort, weil sie weiß, welche Aggregate beteiligt waren und ob die Transaktion erfolgreich war. Der Autor skizziert eine einfache synchrone Schleife, warnt aber: Diese Implementierung verbessert die logische Trennung, bleibt aber synchron – die Anfrage wartet auf jeden Handler, die Zustellung ist nicht beständig. Ein fehlschlagender Handler kann die anderen beeinträchtigen. Für asynchrone, zuverlässige Zustellung empfiehlt er das Transactional Outbox-Pattern. Handler sollten trotzdem idempotent sein – also bei Wiederholung keine unerwünschten Seiteneffekte erzeugen.

Manche Handler rufen externe Systeme auf, andere lösen Verhalten in anderen Aggregaten aus. Wenn ein Handler die Reservierung von Lagerbestand anstoßen soll, muss er das Inventory-Aggregat laden, dessen Methode reserve() aufrufen und eine neue Transaktion abschließen. Das ist eine zweite Transaktion, keine Erweiterung der ersten. Für einen kurzen Moment existiert die Bestellung, ohne dass der Lagerbestand reserviert ist – die beiden Aggregate sind eventuell konsistent. Eine einzelne Transaktion über beide Aggregate hinweg mag manchmal gerechtfertigt sein, koppelt aber ihre Lebenszyklen. Die Faustregel lautet: Ein Aggregat pro Transaktion. Werden zwei Aggregate in derselben Transaktion geändert, deutet das eher auf ein Designproblem im Domänenmodell hin. Die lose Kopplung verbessert auch die Verfügbarkeit: Der Bestellvorgang kann weiter Bestellungen annehmen, während das Inventory über Engpässe entscheidet – nicht verfügbarer Bestand verzögert dann die Auslieferung, führt aber nicht zur Ablehnung der Bestellung.

Sobald ein Event die Grenze eines Bounded Context überschreitet, wird daraus ein Integration Event. Ein Domain Event gehört zum Modell, das es erzeugt hat. Sein Name und seine Struktur können Annahmen enthalten, die im Ordering-Kontext Sinn ergeben, im Fulfillment-Kontext aber nicht. Deshalb übersetzt der empfangende Kontext das Integration Event an seiner Grenze in einen lokalen Befehl – etwa PrepareOrder. Er muss weder die Aggregate noch das interne Event-Modell des Ordering-Kontexts verstehen. Diese Übersetzung muss nicht eins zu eins sein: Ein Kontext kann mehrere Domain Facts kombinieren und für verschiedene Empfänger unterschiedliches Verhalten auslösen. Bounded Contexts sind oft als separate Dienste deployed, aber das muss nicht sein. Sind sie es, werden Integration Events typischerweise asynchron über Messaging-Infrastruktur wie Kafka, RabbitMQ oder Azure Service Bus zugestellt. Das bringt Serialisierung, Zustellungsfehler, Duplikate und versionierte Verträge mit sich. Eine zuverlässige Veröffentlichung erfordert dann meist das Transactional Outbox oder einen gleichwertigen Mechanismus.

Nicht jede Zustandsänderung verdient ein Event. Der Autor warnt davor, für jeden Setter oder Datenbank-Update ein Event zu erzeugen – das erzeugt Rauschen statt eines nützlichen Modells. Ein direkter Aufruf über einen Domänen- oder Anwendungsdienst ist vorzuziehen, wenn die Abhängigkeit Teil einer klaren Operation ist und nichts davon profitiert, getrennt zu werden. Domain Events sollten bedeutsame Fakten im Geschäftsbereich abbilden und keine Abhängigkeiten verstecken, die explizit besser zu verstehen wären.

Praktisch heißt das: Domain Events übersetzen die Logik deines Systems in die Sprache des Fachbereichs und reduzieren gleichzeitig die technische Kopplung. Sie zwingen dich, die Frage „Was ist eigentlich passiert?“ präzise zu beantworten und jede Reaktion unabhängig zu behandeln. Starte mit dem Fakt, den das Geschäft interessiert. Erzeuge das Domain Event genau dort, wo die Entscheidung fällt. Reagiere mit fokussierten Handlern. Übersetze an Kontextgrenzen in Integration Events. Wähle die Zustellungsart – synchron oder asynchron – erst, wenn die Anforderungen an Transaktionen, Konsistenz und Fehlertoleranz klar sind. Der Domain-Fakt bleibt explizit, jede Konsequenz hat ihren eigenen Ort zum Scheitern, Wiederholen und Weiterentwickeln – die Tür zur Verteilung über mehrere Prozesse oder Dienste bleibt offen, wenn das System es verlangt.

Quelle: deniskyashif.com

Deine Reaktion:
Artikel teilen:
Krötzsch-Check0 — 100
Fakten 75
Relevanz 80
Hype 10
Einschätzung 70
Redaktion 50 Stand 50 · noch keine Stimmen
Ist das Hype?
Sebastian Krötzsch
Autor

Sebastian Krötzsch

Sebastian Krötzsch schreibt auf sebask.de über Künstliche Intelligenz, Automatisierung, digitale Systeme und die Frage, was davon im Alltag wirklich nützlich ist. Ohne Buzzword-Nebel, dafür mit klarem Blick auf Praxis, Tools und echte Wirkung.