Lösungsstrategie
Aggregate mit Entitäten aus unterschiedlichen Quellen
Motivation
In komplexen Domänen kommt es vor, dass ein Aggregat Entitäten enthält, deren Daten aus unterschiedlichen Quellen stammen.
Ein klassisches Beispiel ist die Adresse einer Bestellung: Sie kann entweder direkt im Aggregat eingebettet sein (z. B. Lieferadresse des Kunden) oder als Referenz auf eine Entität aus einem anderen Bounded Context vorliegen (z. B. ein externer Abholort aus dem Logistik-Kontext).
Die Herausforderung besteht darin, die fachliche Konsistenz und technische Entkopplung sicherzustellen, ohne dass der Client wissen muss, wie oder woher die jeweilige Entität geladen wird.
| Aspekt | Beschreibung |
|---|---|
Einsatzbereich |
Wenn ein Aggregat eine Entität enthält, die entweder als eingebettete (ad-hoc) Entität bzw. Value Object modelliert ist oder als Referenz auf ein anderes Aggregat aus einem anderen Bounded Context vorliegt. |
Ziel |
Der Client des Domänenobjekts soll nicht selbst entscheiden müssen, wie die zugrunde liegende Entität oder Aggregatreferenz aufgelöst wird. |
Um dem Client die Nutzung zu vereinfachen und das Wissen über unterschiedliche Quellen zu kapseln, übernimmt das Aggregat selbst die Auflösung der konkreten Instanz. Dies geschieht über Callback-Funktionen, die dem Aggregat bei seiner Erzeugung durch den Domain Service übergeben werden. Dadurch bleibt das Aggregat anämisch und frei von Infrastrukturabhängigkeiten. Zur Optimierung der Performance wird das Ergebnis der ersten Auflösung im Aggregat zwischengespeichert; der Client kann bei Bedarf jedoch ein erneutes Laden erzwingen, um aktuelle Daten sicherzustellen.
Im folgenden Beispiel kann eine Bestellung entweder eine eigene Adresse (als eingebettetes Value Object innerhalb des Aggregats) oder eine Referenz auf einen externen Abholort enthalten. Der Client soll dabei nicht wissen müssen, aus welcher Quelle die Adresse stammt – das Aggregat löst diese eigenständig auf.
public Adresse getAdresse() {
return getAdresse(false);
}
public Adresse getAdresse(boolean forceUpdate) {
if (!forceUpdate) {
return Optional.ofNullable(cachedAdresse.get())
.orElseGet(this::loadAdresse);
}
return loadAdresse();
}
private Adresse loadAdresse() {
Adresse neueAdresse = (abholortReferenz != null)
? abholortSupplier.apply(abholortReferenz)
: adresseSupplier.apply(bestellnummer);
cachedAdresse.set(neueAdresse);
return neueAdresse;
}
Ein weiterer Vorteil dieser Implementierung ist, dass die Adresse einer Bestellung automatisch lazy loaded wird. Unabhängig davon, ob sie aus dem lokalen Aggregat oder über einen Adapter aus einem anderen Bounded Context geladen wird, erfolgt der Zugriff erst beim Aufruf des Getters. Dies führt zu einer sauberen Entkopplung und einer effizienten Ressourcennutzung.
Die Verwendung von AtomicReference ermöglicht dabei eine sichere, konsistente Verwaltung des zwischengespeicherten Werts auch, wenn das Aggregat in einem Kontext mit parallelen Zugriffen genutzt wird. Sie stellt sicher, dass die Adresse nur einmal geladen und danach zuverlässig wiederverwendet wird, ohne dass externe Synchronisierung notwendig ist.
Die vollständige Implementierung des Aggregats ist hier zu finden: hier.
Beim Erzeugen des Aggregats im Domain Service – in diesem Beispiel im BestellungDomainService.java – werden die entsprechenden Callbacks gesetzt, sodass die Adresse zur Laufzeit ermittelt werden kann:
@Override
public Bestellung query(Bestellnummer bestellnummer) throws BestellungNotFoundException {
if (bestellnummer == null) {
throw new IllegalArgumentException("Bestellung ohne Nummer kann nicht abgerufen werden");
}
final BestellungsabfrageEvent bestellungsabfrageEvent = bestellungRepository.find(bestellnummer);
return new Bestellung(
bestellungsabfrageEvent,
adresseService::getAdresse,
adresseService::getAbholort
);
}
Domänen-Events zur Steigerung der Ausdrucksstärke
/**
* Ereignisse die ein fahcliches Event / Veränderung repräsentieren
*/
public interface DomainEvent {
}
Da in diesem Szenario ein Aggregat Entitäten aus unterschiedlichen Quellen (eingebettet oder referenziert) abbildet, muss auch die Erzeugung des Aggregats angepasst werden.
Dies erfolgt über die Verwendung von DomainEvent, um sowohl die Entstehung als auch die fachliche Korrektheit des Domänenobjekts klar auszudrücken.
Die Validierung der Fachlichkeit verbleibt dabei konsequent im Domänenkern.
Das folgende Beispiel zeigt die Erzeugung des Domänenobjekts im Kontext der Bestellung:
private Bestellnummer createFromEvent(BestellungsaufgabeEvent bestellungsaufgabeEvent) throws BestellungException {
if (bestellungsaufgabeEvent == null) {
throw new IllegalArgumentException("Bestellung darf nicht leer sein");
}
final Bestellnummer bestellnummer = new Bestellnummer(UUID.randomUUID().toString());
Bestellung.validate(bestellnummer, bestellungsaufgabeEvent);
bestellungRepository.insert(bestellnummer, bestellungsaufgabeEvent);
return bestellnummer;
}
@Override
public Bestellnummer create(BestellungsaufgabeEventMitAbholort bestellungsaufgabeEventMitAbholort) throws BestellungException {
return createFromEvent(bestellungsaufgabeEventMitAbholort);
}
@Override
public Bestellnummer create(BestellungsaufgabeEventMitAdresse bestellungsaufgabeEventMitAdresse) throws BestellungException {
return createFromEvent(bestellungsaufgabeEventMitAdresse);
}
Bausteinsicht
Nachfolgend die Bausteinsicht, der einzelnen Elemente und deren Interaktionen.
Domain Serivce
Entwurfsentscheidungen
Architekturentscheidung: Aggregate mit Entitäten aus unterschiedlichen Quellen
Datum |
2025-10-17 |
Status |
beschlossen |
Verantwortlich |
Architekturteam / Domänenarchitekt |
Version |
1.0 |
Kontext und Problemstellung
In der Domäne E-Commerce soll das Aggregat Bestellung eine Adresse enthalten, die aus unterschiedlichen Quellen stammen kann:
-
entweder als eingebettetes Value Object innerhalb des Aggregats (z. B. Lieferadresse des Kunden),
-
oder als Referenz auf ein externes Aggregat (z. B. Abholort aus dem Logistik-Kontext).
Das Aggregat darf dabei nicht von Infrastruktur- oder Integrationslogik abhängen. Zugleich soll der Client (z. B. Application Service) nicht selbst wissen müssen, wie oder woher die Adresse zu laden ist.
Entscheidung
Das Aggregat löst die Entität eigenständig über Callback-Funktionen auf, die bei seiner Erzeugung vom Domain Service übergeben werden. Dadurch kann die konkrete Quelle der Entität (lokal oder extern) zur Laufzeit bestimmt werden, ohne die Domäne zu verunreinigen.
Die Adressdaten werden lazy geladen – also erst dann abgerufen, wenn der Client auf die Adresse zugreift. Dies ermöglicht eine effiziente Ressourcennutzung und entkoppelt die Lebenszyklen der beteiligten Kontexte.
Begründung
-
Das Aggregat bleibt rein domänenorientiert (keine Infrastrukturabhängigkeit).
-
Durch Lazy Loading entsteht eine performante, bedarfsgesteuerte Datenbeschaffung.
-
Die Callback-Strategie erlaubt es, externe Datenquellen flexibel einzubinden (z. B. Logistikservice, Kundendatenbank).
-
Domain Events können zusätzlich genutzt werden, um die Entstehung und Konsistenz des Aggregats explizit darzustellen.
Konsequenzen
Vorteile:
-
Saubere Trennung von Domäne und Infrastruktur
-
Hohe Ausdrucksstärke des Domänenmodells
-
Effiziente Datenbeschaffung (Lazy Loading)
-
Geringer Kopplungsgrad zwischen Bounded Contexts
Nachteile / Risiken:
-
Lazy Loading erschwert Debugging und Monitoring
-
Potenzielle Netzwerkaufrufe bei Zugriff (Latenzrisiko)
-
Erhöhter Testaufwand (Callbacks müssen gemockt werden)
-
Zusätzliche Fehlerbehandlung bei Callback-Auflösung notwendig
Alternativen
-
Direkte Auflösung der Referenzen durch den Client → würde die Entkopplung verletzen und Infrastrukturwissen in den Application Layer verlagern.
-
Synchrones Laden aller abhängigen Entitäten bei Erzeugung des Aggregats → würde die Performance verschlechtern und Aggregatgrößen künstlich aufblähen.
Bewertung
Der gewählte Ansatz bietet ein gutes Gleichgewicht zwischen Kapselung, Ausdrucksstärke und Flexibilität. Er eignet sich besonders für Szenarien, in denen Aggregate sowohl eingebettete als auch referenzierte Entitäten enthalten und die Quelle erst zur Laufzeit bestimmt werden soll.