
In dieser Mini-Serie schaue ich mir die Building Blocks im Domain-driven Design (DDD) an. Diese Bausteine gehören zum Tactical Design des DDD. Abgrenzend gibt es das Strategic Design, welches sich mit den High-Level-Aspekten der Domäne als Ganzes befasst, wie der Ubiquituos Language, Bounded Contexts und Context Maps. Tactical Design ist der Hands-On-Part im DDD und die Building Blocks stellen konkrete Möglichkeiten dar, um DDD in Code zu gießen.
- Domain-driven Design: Value Object
- Domain-driven Design: Entity, Aggregate und Domain Service
- Domain-driven Design: Factory und Repository
- Domain-driven Design: Domain Event
Dieser letzte Teil der Serie befasst sich mit dem Domain Event.
Ein Domain Event repräsentiert ein wichtiges fachliches Ereignis, das stattgefunden hat. Es ist immer eindeutig einem Aggregate zugeordnet.
Entkopplung von Geschäftsprozessen
In Geschäftsanwendungen, die auf Domain-driven Design verzichten, finden sich vielfach lange Abfolgen von If-Else-Verschachtelungen, die beschreiben, was unter welchen Umständen getan werden soll, nachdem etwas angelegt, aktualisiert oder gelöscht worden ist oder wenn eine Statustransition stattgefunden hat, häufig mit diversen Ausnahmen und Ausnahmen von Ausnahmen. Derartige Programmabläufe sind oft sehr unleserlich, bergen die Gefahr von totem Code und Programmierfehlern und lassen sich auch nur schwer in eine neue Anwendung überführen.
Ein Ansatz zum Umgang mit diesen Herausforderungen ist das Domain Event. Es schafft eine Entkopplung, indem es beschreibt, was passiert ist, aber nicht, was getan werden soll. Es beinhaltet alle relevanten Kontextinformationen und erlaubt es interessierten Prozessen, das Ereignis zu verarbeiten, ohne dass eine Abhängigkeit vom Domain Event zu diesen Prozessen entsteht.
Event vs. Command
Ein Event, zu Deutsch Ereignis, beschreibt etwas, das bereits passiert ist. Ein Command, zu Deutsch Befehl, beschreibt eine Aktion, die durchgeführt werden soll. Event und Command sind hinsichtlich Zweck und Inhalt gegensätzlich, was beim Design berücksichtigt werden muss. Eine gute Namenswahl für ein Domain Event ist das betroffene Aggregate bzw. eine diesem untergeordnete Entity plus das Verb, das die durchgeführte fachliche Aktion beschreibt, im Partizip II (auch Partizip Perfekt).
Für ein Aggregate Auftrag, das eine Liste von Entities Auftragsposition hält, kann ein Ereignis, das den erfolgreichen Abschluss eines Auftrags beschreibt, AuftragAbgeschlossen lauten. Ein Ereignis, dass das Entfernen einer Auftragsposition beschreibt, kann AuftragspositionEntfernt heißen.
Beide Ereignisse haben nur dann eine Daseinsberechtigung, wenn sie potenziell Prozesse außerhalb des Auftrag-Aggregates anstoßen. Es ist zum Beispiel vorstellbar, dass bei Abschluss eines Auftrags Statistiken geschrieben werden oder dass bei Entfernen einer Auftragsposition ebenso wie beim Hinzufügen einer solchen Bestandsinformationen aktualisiert werden.
Entscheidend beim inhaltlichen Entwurf eines Domain Events ist, dass es die potenziell relevanten Kontextinformationen enthält und nicht auf einen an dem Event interessierten Service zugeschnitten ist. Das bedeutet auch, dass nicht alle in dem Event gespeicherten Informationen für jeden Interessenten relevant sind.
Erzeugung und Verarbeitung
Domain Events beziehen sich auf ein Aggregate und werden aus diesem heraus erzeugt. Die Publizierung der Events kann nach dem Entwurfsmuster Observer stattfinden. Interessierte Services registrieren sich an dem Aggregate und werden über neue Events benachrichtigt.
Jeder Interessent bekommt dabei alle Events und kann unabhängig von den anderen Interessenten darauf reagieren. Domain Events sind damit ideal um Änderungen am fachlichen Application State über Bounded Contexts hinweg bekannt zu machen. In einer Microservice-Landschaft könnte zum Beispiel ein dedizierter Service alle Events in eine Message Queue schreiben, die von allen interessierten Services abonniert wird. In einem Monolithen kann ein Service aus einem anderen Modul direkt das Aggregate abbonieren, wobei die Kopplung sich auf den Inhalt des Domain Events beschränkt und damit minimal bleibt.
Je nach Komplexität und Technologie kann es sinnvoll sein, dass sich Interessenten nicht direkt am Aggregate registrieren, sondern ein dedizierter Service alle Aggregates kennt und den Interessenten diese Funktionalität gebündelt zur Verfügung stellt.
Domain Events mit Spring Boot abbilden
Für Java– und Kotlin-Entwickler bietet Spring Boot eine eigene Implementierung von Domain Events. Das Aggregate speichert dafür alle Domain Events in einer Collection, die mit @DomainEvents
annotiert ist. Eine mit @AfterDomainEventPublication
annotierte Methode räumt die Collection nach der Verarbeitung auf, damit jedes Event nur einmal verarbeitet wird.
Interessierte Services implementieren eine Methode, die das DomainEvent entgegenimmt. Die Annotation @EventListener
an der Methode sorgt dafür, dass für jedes im Aggregate erzeugte Event diese Methode aufgerufen wird.
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Auftrag {
private List<DomainEvent> domainEventList = new ArrayList<>();
public static Auftrag erzeugen() {
final var auftrag = new Auftrag();
auftrag.getDomainEvents().add(new AuftragErzeugtEvent(auftrag));
return auftrag;
}
@DomainEvents
Collection<DomainEvent> getDomainEvents() {
return domainEventList;
}
@AfterDomainEventPublication
void clearDomainEvents() {
domainEventList.clear();
}
}
Die Klasse Auftrag erstellt bei der Erzeugung eines neuen Auftrags über eine statische Factory-Methode ein neues AuftragErzeugtEvent
und speichert es in der Liste der Domain Events. Über getDomainEvents()
wird die Liste entsprechend annotiert nach außen zugänglich gemacht. Die Methode clearDomainEvents()
wird durch die entsprechende Annotation nach Verarbeitung der Domain Events vom Spring-Framework aufgerufen, um bereits verarbeitete Domain Events nicht noch einmal zu publizieren.
@RequiredArgsConstructor
public class AuftragErzeugtEvent implements DomainEvent {
@Getter
private final Auftrag auftrag;
}
Das AuftragErzeugtEvent
implementiert ein Interface DomainEvent
, damit sämtliche Domain Events des Aggregates in der gleichen Collection vorgehalten werden können. Der Einfachheit halber kapselt es den kompletten Auftrag, wobei hier natürlich die Immutability verloren geht und die Gefahr besteht, dass bei der Eventverarbeitung Geschäftslogik am Auftrag ausgeführt werden kann, was in der Regel nicht erwünscht sein dürfte. Besser ist es die relevanten Properties des Auftrags in eine unveränderbare Modellstruktur zu überführen, die das Event dann bereitstellt.
@Slf4j
@Component
public class AuftragEventHandler {
@EventListener
public void handleEvent(final AuftragErzeugtEvent auftragErzeugtEvent) {
log.info("Auftrag {} erzeugt", auftragErzeugtEvent.getAuftrag());
}
}
Die Methode handleEvent()
nimmt das AuftragErzeugtEvent
entgegen. Durch die Annotation @EventListener
wird gewährleistet, dass Spring diese Methode mit allen AuftragErzeugtEvents aus der Collection am Auftrag-Aggregate aufruft. Die Methode dient nur der Veranschaulichung und tut daher nicht mehr, als den Auftrag zu loggen, wobei dieser im Beispiel noch nicht einmal eine ausimplementierte toString-Methode besitzt.
Prinzipiell könnte die Methode auch das Interface DomainEvent
als Argument entgegennehmen, dann würden sämtliche Domain Events von einer Methode abgehandelt werden. Ebenso ist es vorstellbar und bei komplexeren Systemen auch ratsam, dass es mehr als einen @EventListener
gibt. Entscheidend dafür ist, dass dem Spring-Framework der jeweilige Code bekannt gemacht wird, etwa über die Annotation @Component
an der Klasse. In den meisten Fällen möchte man die Reaktion auf ein Event an eine erfolgreiche Transaktion koppeln und kann dafür die Annotation @TransactionalEventListener
einsetzen, die eine Erweiterung von @EventListener
darstellt.