Domain-Driven Design
Software-Muster jenseits von GoF · Gruppe 4 von 7
Eric Evans hat 2003 mit Domain-Driven Design beschrieben, wie man Software um das Fachgebiet herum baut statt um die Technik. Ein Kernpunkt ist eine gemeinsame Sprache von Fachleuten und Entwicklern, die sich direkt im Code wiederfindet. Dazu hat Evans eine Reihe taktischer Bausteine beschrieben. Das Value Object und die Specification stehen bereits in Gruppe 2, das Repository in Gruppe 3. Diese Gruppe ergänzt die übrigen fünf.
Die Codebeispiele bauen aufeinander auf und verwenden durchgehend den Online-Händler mit Kunden, Bestellungen und Konten.
- Anwendung / Objekt
- Muster-Baustein
- Speicher / Ressource
22. Entity · Entität
Die Idee
Eine Entity ist ein Objekt, das sich über eine eigene, dauerhafte Identität definiert, nicht über seine Eigenschaften. Ein Kunde kann heiraten, umziehen und eine neue Telefonnummer bekommen und bleibt doch derselbe Kunde. Zwei Kunden mit identischem Namen und Wohnort sind dagegen zwei verschiedene Menschen. Das ist der Gegenpol zum Value Object (Gruppe 2), das allein über seine Werte definiert ist.
Das Problem
Vergleicht man Kunden über ihre Daten, gilt eine Kundin nach dem Umzug als „neuer“ Kunde, ihre Bestellhistorie scheint verloren. Und zwei Namensvettern würden zu einer Person verschmolzen. Wird Gleichheit gar nicht definiert, gelten zwei aus der Datenbank geladene Exemplare derselben Kundin als verschieden.
Im Code
Die Klasse Kunde hat eine unveränderliche KundenId, selbst ein Value Object, und veränderliche Eigenschaften. equals und hashCode vergleichen nur die Id. Ein alter und ein neuer Stand derselben Kundin sind gleich, zwei Kundinnen mit gleichen Daten aber verschiedenen Ids sind es nicht. Im Set landen deshalb zwei Einträge, nicht drei.
Java-21-Code: Entity.java
Direkt ausführbar mit java Entity.java
// Entity
// Ein Objekt mit eigener, dauerhafter Identität. Seine Eigenschaften können sich ändern, es bleibt dasselbe.
// Start: java Entity.java
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
public class Entity {
// --- Value Objects (vgl. Gruppe 2): gleich, wenn die Werte gleich sind ---
record KundenId(UUID wert) {
static KundenId neu(String schluessel) { return new KundenId(UUID.nameUUIDFromBytes(schluessel.getBytes())); } // im echten System: UUID.randomUUID()
public String toString() { return wert.toString().substring(0, 8); }
}
record Adresse(String strasse, String ort) {}
// --- Entity: gleich, wenn die Identität gleich ist ---
static final class Kunde {
private final KundenId id; // ändert sich nie
private String name; // darf sich ändern
private Adresse adresse; // darf sich ändern
Kunde(KundenId id, String name, Adresse adresse) {
this.id = Objects.requireNonNull(id);
this.name = name;
this.adresse = adresse;
}
KundenId id() { return id; }
void ziehtUm(Adresse neu) { this.adresse = neu; }
void heiratet(String neuerName) { this.name = neuerName; }
@Override public boolean equals(Object o) { return o instanceof Kunde k && k.id.equals(id); }
@Override public int hashCode() { return id.hashCode(); }
@Override public String toString() { return "Kunde " + id + ": " + name + ", " + adresse.ort(); }
}
public static void main(String[] args) {
KundenId id = KundenId.neu("anna-1");
Kunde anna = new Kunde(id, "Anna Lenz", new Adresse("Domweg 3", "Köln"));
System.out.println("Vorher: " + anna);
anna.heiratet("Anna Berg");
anna.ziehtUm(new Adresse("Hafenstraße 7", "Hamburg"));
System.out.println("Nachher: " + anna);
// Gleiche Identität = gleiche Entity, auch wenn die Daten verschieden sind (z. B. alter Stand aus der DB)
Kunde alterStand = new Kunde(id, "Anna Lenz", new Adresse("Domweg 3", "Köln"));
System.out.println("\nalter Stand equals aktueller Stand: " + alterStand.equals(anna));
// Gleiche Daten, andere Identität = verschiedene Entities (zwei Kundinnen gleichen Namens)
Kunde namensvetterin = new Kunde(KundenId.neu("anna-2"), "Anna Berg", new Adresse("Hafenstraße 7", "Hamburg"));
System.out.println("gleiche Daten, andere Id, equals: " + namensvetterin.equals(anna));
Set<Kunde> kunden = new HashSet<>(Set.of(anna, namensvetterin));
kunden.add(alterStand);
System.out.println("Kunden im Set: " + kunden.size());
// Zum Vergleich: Value Objects vergleichen über Werte
System.out.println("\nAdresse equals gleiche Adresse: " +
new Adresse("Domweg 3", "Köln").equals(new Adresse("Domweg 3", "Köln")));
}
}Ausgabe
Vorher: Kunde 7e2cb236: Anna Lenz, Köln
Nachher: Kunde 7e2cb236: Anna Berg, Hamburg
alter Stand equals aktueller Stand: true
gleiche Daten, andere Id, equals: false
Kunden im Set: 2
Adresse equals gleiche Adresse: trueWorauf achten
Die wichtigste Entscheidung ist die Identität: Woher kommt sie, und ist sie wirklich dauerhaft? E-Mail-Adresse oder Name sind schlechte Kandidaten, weil sie sich ändern. Bewährt sind künstliche Schlüssel wie UUIDs oder fortlaufende Nummern. Entities sollten nicht als Records modelliert werden, weil Records über alle Felder vergleichen. Und nicht alles ist eine Entity: Viele Dinge, die man spontan als Entity anlegt, sind eigentlich Value Objects. Die Frage lautet immer: Interessiert uns, welches Exemplar das ist, oder nur, was es enthält?
In der Praxis
JPA-Klassen mit @Entity und @Id. Die JPA-Entity ist allerdings ein technischer Begriff für „hat eine Tabelle“ und nicht dasselbe wie die DDD-Entity. Auch Adressen werden in JPA oft als @Entity angelegt, obwohl sie fachlich Value Objects sind. Dafür gibt es @Embeddable.
23. Aggregate · Aggregat
Die Idee
Ein Aggregat ist eine Gruppe zusammengehöriger Objekte, die als Einheit behandelt wird, etwa eine Bestellung mit ihren Positionen. Eines der Objekte ist die Wurzel (Aggregate Root). Nur über sie darf von außen auf die Gruppe zugegriffen werden. Die Wurzel stellt sicher, dass die Regeln der Gruppe, die Invarianten, jederzeit eingehalten sind. Gespeichert und geladen wird immer das ganze Aggregat.
Das Problem
Darf jeder Programmteil Bestellpositionen direkt anlegen und ändern, kann niemand garantieren, dass eine Bestellung höchstens fünf Positionen und höchstens 5.000 € hat oder dass nach dem Bezahlen nichts mehr hinzukommt. Die Prüfungen müssten an jeder Stelle wiederholt werden, und eine vergessene Prüfung reicht für inkonsistente Daten.
Im Code
Die Bestellung ist die Wurzel. Positionen kommen nur über fuegeHinzu() hinein, das alle Regeln prüft. Nach außen gibt es die Positionen nur als unveränderliche Liste. Der Versuch, dort etwas einzuschleusen, scheitert. Auf den Kunden, ein eigenes Aggregat, verweist die Bestellung nur über dessen KundenId, nicht über das Objekt.
Java-21-Code: Aggregate.java
Direkt ausführbar mit java Aggregate.java
// Aggregate
// Eine Gruppe zusammengehöriger Objekte mit einer Wurzel, die alle Regeln der Gruppe sicherstellt.
// Start: java Aggregate.java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Aggregate {
record KundenId(long wert) {} // Verweis auf ein ANDERES Aggregat: nur die Id
record ArtikelNr(String wert) {}
// --- Teil des Aggregats: von außen nicht direkt änderbar ---
record Position(ArtikelNr artikel, int menge, long einzelpreisCent) {
long summe() { return menge * einzelpreisCent; }
}
enum Status { OFFEN, BEZAHLT, VERSANDT }
// --- Aggregatwurzel: einziger Zugang, schützt die Regeln (Invarianten) ---
static final class Bestellung {
static final int MAX_POSITIONEN = 5;
static final long MAX_SUMME_CENT = 500_000;
private final long id;
private final KundenId kunde;
private final List<Position> positionen = new ArrayList<>();
private Status status = Status.OFFEN;
Bestellung(long id, KundenId kunde) { this.id = id; this.kunde = kunde; }
void fuegeHinzu(ArtikelNr artikel, int menge, long einzelpreisCent) {
pruefe(status == Status.OFFEN, "Bestellung ist nicht mehr offen");
pruefe(menge > 0, "Menge muss positiv sein");
pruefe(positionen.size() < MAX_POSITIONEN, "höchstens " + MAX_POSITIONEN + " Positionen");
Position neu = new Position(artikel, menge, einzelpreisCent);
pruefe(summe() + neu.summe() <= MAX_SUMME_CENT, "Bestellwert über 5.000 €");
positionen.add(neu);
}
void bezahle() { pruefe(status == Status.OFFEN && !positionen.isEmpty(), "nichts zu bezahlen"); status = Status.BEZAHLT; }
void versende() { pruefe(status == Status.BEZAHLT, "erst bezahlen, dann versenden"); status = Status.VERSANDT; }
long summe() { return positionen.stream().mapToLong(Position::summe).sum(); }
List<Position> positionen() { return Collections.unmodifiableList(positionen); } // nur lesend nach außen
Status status() { return status; }
private static void pruefe(boolean ok, String regel) {
if (!ok) throw new IllegalStateException(regel);
}
public String toString() {
return "Bestellung " + id + " (Kunde " + kunde.wert() + "): " + positionen.size() + " Pos., "
+ summe() / 100 + " €, " + status;
}
}
static void versuche(String was, Runnable r) {
try { r.run(); System.out.println(" ok: " + was); }
catch (RuntimeException e) {
System.out.println(" abgelehnt: " + was + " -> " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()));
}
}
public static void main(String[] args) {
Bestellung b = new Bestellung(4711, new KundenId(815));
versuche("Buch hinzufügen", () -> b.fuegeHinzu(new ArtikelNr("B-1"), 2, 1_990));
versuche("Laptop hinzufügen", () -> b.fuegeHinzu(new ArtikelNr("L-9"), 1, 89_900));
versuche("5 Fernseher hinzufügen", () -> b.fuegeHinzu(new ArtikelNr("TV-3"), 5, 99_900));
versuche("Menge 0", () -> b.fuegeHinzu(new ArtikelNr("B-2"), 0, 990));
versuche("Position von außen einschleusen", () -> b.positionen().add(new Position(new ArtikelNr("X"), 1, 1)));
versuche("versenden vor dem Bezahlen", b::versende);
versuche("bezahlen", b::bezahle);
versuche("noch etwas hinzufügen", () -> b.fuegeHinzu(new ArtikelNr("B-3"), 1, 990));
versuche("versenden", b::versende);
System.out.println("\n" + b);
}
}Ausgabe
ok: Buch hinzufügen
ok: Laptop hinzufügen
abgelehnt: 5 Fernseher hinzufügen -> Bestellwert über 5.000 €
abgelehnt: Menge 0 -> Menge muss positiv sein
abgelehnt: Position von außen einschleusen -> UnsupportedOperationException
abgelehnt: versenden vor dem Bezahlen -> erst bezahlen, dann versenden
ok: bezahlen
abgelehnt: noch etwas hinzufügen -> Bestellung ist nicht mehr offen
ok: versenden
Bestellung 4711 (Kunde 815): 2 Pos., 938 €, VERSANDTWorauf achten
Vaughn Vernon hat für Aggregate vier Faustregeln formuliert: Klein halten, also nur zusammenfassen, was wirklich gemeinsam konsistent sein muss. Andere Aggregate nur per Id referenzieren, nicht per Objektverweis. Pro Transaktion nur ein Aggregat ändern. Und zwischen Aggregaten nur letztendlich konsistent sein, etwa über Domain Events. Ein häufiger Fehler sind riesige Aggregate, bei denen ein Kunde alle seine Bestellungen enthält. Dann sperrt jede neue Bestellung den ganzen Kunden.
In der Praxis
Pro Aggregat gibt es genau ein Repository (Gruppe 3). In JPA wird die Wurzel zur Entity mit Kaskadierung auf die Teile (cascade = ALL, orphanRemoval = true). Frameworks wie Axon oder jMolecules unterstützen das Konzept direkt.
24. Domain Event · Fachereignis
Die Idee
Ein Domain Event hält fest, dass fachlich etwas Bedeutsames passiert ist: eine Bestellung wurde aufgegeben, eine Zahlung ist eingegangen, ein Kunde ist umgezogen. Das Ereignis ist ein unveränderliches Objekt, benannt in der Vergangenheit, mit allen wichtigen Angaben. Andere Teile des Systems reagieren darauf, ohne dass das auslösende Objekt sie kennt.
Das Problem
Würde die Bestellung beim Aufgeben selbst das Lager, den Mailversand und das Protokoll aufrufen, wäre sie mit all diesen Bereichen verdrahtet. Jede neue Reaktion, etwa ein Bonusprogramm, erforderte eine Änderung an der Bestellung. Und nach der Aggregat-Regel „nur ein Aggregat pro Transaktion“ darf sie andere Aggregate gar nicht direkt ändern.
Im Code
Die Ereignisse sind Records hinter einem sealed Interface. Die Bestellung sammelt sie in einer Liste. Erst nach dem (hier gedachten) Speichern werden sie entnommen und über einen einfachen Bus an die Abonnenten verteilt. Ein Abonnent wertet sie per Pattern Matching im switch aus, ein anderer mit einem Record Pattern in instanceof.
Java-21-Code: DomainEvent.java
Direkt ausführbar mit java DomainEvent.java
// Domain Event
// Ein fachlich bedeutsames Ereignis wird als Objekt festgehalten und an Interessierte verteilt.
// Start: java DomainEvent.java
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class DomainEvent {
// --- Die Ereignisse: unveränderlich, in der Vergangenheit benannt ---
sealed interface Ereignis permits BestellungAufgegeben, BestellungStorniert {
long bestellung();
Instant zeitpunkt();
}
record BestellungAufgegeben(long bestellung, String kunde, long summeCent, Instant zeitpunkt) implements Ereignis {}
record BestellungStorniert(long bestellung, String grund, Instant zeitpunkt) implements Ereignis {}
// --- Aggregat: zeichnet Ereignisse auf, statt andere direkt aufzurufen ---
static final class Bestellung {
final long id;
final String kunde;
long summeCent;
boolean aufgegeben, storniert;
private final List<Ereignis> neueEreignisse = new ArrayList<>();
Bestellung(long id, String kunde, long summeCent) { this.id = id; this.kunde = kunde; this.summeCent = summeCent; }
void aufgeben() {
aufgegeben = true;
neueEreignisse.add(new BestellungAufgegeben(id, kunde, summeCent, Instant.parse("2026-10-27T10:15:00Z")));
}
void stornieren(String grund) {
storniert = true;
neueEreignisse.add(new BestellungStorniert(id, grund, Instant.parse("2026-10-27T10:40:00Z")));
}
List<Ereignis> entnehmeEreignisse() {
List<Ereignis> e = List.copyOf(neueEreignisse);
neueEreignisse.clear();
return e;
}
}
// --- Einfacher Verteiler für Ereignisse ---
static final class Ereignisbus {
private final List<Consumer<Ereignis>> abonnenten = new ArrayList<>();
void abonniere(Consumer<Ereignis> a) { abonnenten.add(a); }
void veroeffentliche(List<Ereignis> ereignisse) { ereignisse.forEach(e -> abonnenten.forEach(a -> a.accept(e))); }
}
public static void main(String[] args) {
Ereignisbus bus = new Ereignisbus();
// Andere Teile des Systems reagieren, ohne dass die Bestellung sie kennt
bus.abonniere(e -> {
switch (e) {
case BestellungAufgegeben a -> System.out.println(" Lager: reserviere Ware für Bestellung " + a.bestellung());
case BestellungStorniert s -> System.out.println(" Lager: gib Ware für Bestellung " + s.bestellung() + " frei");
}
});
bus.abonniere(e -> {
if (e instanceof BestellungAufgegeben(long nr, String kunde, long summe, Instant zeit))
System.out.printf(" E-Mail: Danke %s, Bestellung %d über %d,%02d €%n", kunde, nr, summe / 100, summe % 100);
});
bus.abonniere(e -> System.out.println(" Protokoll: " + e.zeitpunkt() + " " + e.getClass().getSimpleName()));
Bestellung b = new Bestellung(4711, "Sven", 12_990);
System.out.println("Bestellung aufgeben:");
b.aufgeben();
// erst NACH erfolgreichem Speichern veröffentlichen
bus.veroeffentliche(b.entnehmeEreignisse());
System.out.println("\nBestellung stornieren:");
b.stornieren("Kunde hat es sich anders überlegt");
bus.veroeffentliche(b.entnehmeEreignisse());
}
}Ausgabe
Bestellung aufgeben:
Lager: reserviere Ware für Bestellung 4711
E-Mail: Danke Sven, Bestellung 4711 über 129,90 €
Protokoll: 2026-10-27T10:15:00Z BestellungAufgegeben
Bestellung stornieren:
Lager: gib Ware für Bestellung 4711 frei
Protokoll: 2026-10-27T10:40:00Z BestellungStorniertWorauf achten
Ereignisse sollten erst nach erfolgreichem Speichern verteilt werden, sonst reagiert das Lager auf eine Bestellung, deren Speicherung danach fehlschlägt. Sollen Ereignisse andere Dienste über Messaging erreichen, hilft das Outbox-Muster (Gruppe 7). Ein Domain Event beschreibt Fachliches: „BestellungAufgegeben“, nicht „DatensatzGeändert“. Es ist die fachliche Seite dessen, was in den Enterprise Integration Patterns als Event Message verschickt wird.
In der Praxis
@DomainEvents und AbstractAggregateRoot in Spring Data, die Ereignisse beim Speichern automatisch veröffentlichen, ApplicationEventPublisher in Spring, Axon Framework. Event Sourcing (Gruppe 5) speichert sogar ausschließlich Domain Events.
25. Domain Service · Fachdienst
Die Idee
Manche Fachlogik passt in keine einzelne Entity, weil sie mehrere gleichrangig betrifft oder weil sie in der Fachsprache als eigener Vorgang gilt. Für sie gibt es einen Domain Service: eine zustandslose Klasse, deren Name ein fachlicher Begriff ist und deren Methoden fachliche Vorgänge ausführen.
Das Problem
Eine Überweisung betrifft zwei Konten. Gehört ueberweise() in das sendende Konto? Dann muss das Konto das empfangende Konto kennen und verändern, was die Aggregat-Regeln verletzt. In das empfangende? Ebenso schlecht. Steckt man die Logik dagegen in irgendeine Hilfsklasse außerhalb der Domäne, geht Fachwissen verloren.
Im Code
Die Konten bleiben für ihre eigenen Regeln zuständig: Ob abgebucht werden darf, entscheidet das Konto selbst anhand von Saldo und Dispo. Der Ueberweisungsdienst kennt die Regeln, die beide betreffen: kein gleiches Konto, positiver Betrag, Gebühr bei Auslandsüberweisungen. Er hat keinen eigenen Zustand, nur eine Konfiguration.
Java-21-Code: DomainService.java
Direkt ausführbar mit java DomainService.java
// Domain Service
// Fachlogik, die zu keiner einzelnen Entity gehört, bekommt einen eigenen, zustandslosen Dienst.
// Start: java DomainService.java
import java.util.Locale;
public class DomainService {
// --- Entities mit eigener Logik ---
static final class Konto {
final String iban;
final String inhaber;
private long saldoCent;
private final long dispoCent;
Konto(String iban, String inhaber, long saldoCent, long dispoCent) {
this.iban = iban; this.inhaber = inhaber; this.saldoCent = saldoCent; this.dispoCent = dispoCent;
}
boolean kannAbbuchen(long cent) { return saldoCent - cent >= -dispoCent; }
void belaste(long cent) {
if (!kannAbbuchen(cent)) throw new IllegalStateException("Deckung nicht ausreichend");
saldoCent -= cent;
}
void schreibeGut(long cent) { saldoCent += cent; }
public String toString() { return String.format(Locale.GERMANY, "%-6s %9.2f €", inhaber, saldoCent / 100.0); }
}
// --- Domain Service: Logik, die zwei Konten betrifft, gehört keinem von beiden ---
static final class Ueberweisungsdienst {
private final long gebuehrCent;
Ueberweisungsdienst(long gebuehrCent) { this.gebuehrCent = gebuehrCent; }
void ueberweise(Konto von, Konto an, long cent) {
if (von == an) throw new IllegalArgumentException("gleiches Konto");
if (cent <= 0) throw new IllegalArgumentException("Betrag muss positiv sein");
long gebuehr = von.iban.substring(0, 2).equals(an.iban.substring(0, 2)) ? 0 : gebuehrCent; // Ausland kostet
von.belaste(cent + gebuehr);
an.schreibeGut(cent);
}
}
public static void main(String[] args) {
Konto sven = new Konto("DE89 3704", "Sven", 50_000, 20_000);
Konto anna = new Konto("DE12 5001", "Anna", 10_000, 0);
Konto marco = new Konto("AT61 1904", "Marco", 0, 0);
var dienst = new Ueberweisungsdienst(150);
System.out.println("Start: " + sven + " | " + anna + " | " + marco);
dienst.ueberweise(sven, anna, 30_000);
System.out.println("Inland: " + sven + " | " + anna + " | " + marco);
dienst.ueberweise(sven, marco, 10_000);
System.out.println("Ausland: " + sven + " | " + anna + " | " + marco + " (1,50 € Gebühr)");
try {
dienst.ueberweise(anna, marco, 50_000);
} catch (IllegalStateException e) {
System.out.println("Abgelehnt: Anna -> Marco 500 €: " + e.getMessage());
}
}
}Ausgabe
Start: Sven 500,00 € | Anna 100,00 € | Marco 0,00 €
Inland: Sven 200,00 € | Anna 400,00 € | Marco 0,00 €
Ausland: Sven 98,50 € | Anna 400,00 € | Marco 100,00 € (1,50 € Gebühr)
Abgelehnt: Anna -> Marco 500 €: Deckung nicht ausreichendWorauf achten
Domain Services sind die Ausnahme, nicht die Regel. Die größte Gefahr ist das „blutleere Modell“ (Anemic Domain Model): Alle Logik wandert in Services, und die Entities bleiben reine Datenbehälter mit Gettern und Settern. Dann ist von objektorientiertem Design nichts mehr übrig. Ein Domain Service ist auch zu unterscheiden vom Application Service, der einen Anwendungsfall koordiniert (laden, Fachlogik aufrufen, speichern, Transaktion) und selbst keine Fachlogik enthält.
In der Praxis
Typische Kandidaten sind Überweisungen, Preis- und Steuerberechnungen über mehrere Objekte, Prüfungen auf Doppelbuchungen, Wechselkursumrechnungen. In Spring sind solche Klassen oft mit @Service markiert, wobei diese Annotation fachlich keinen Unterschied zwischen Domain und Application Service macht.
26. Anti-Corruption Layer · Schutzschicht
Die Idee
Muss das eigene System mit einem fremden zusammenarbeiten, etwa einem Altsystem, einer eingekauften Software oder dem System eines Partners, setzt man eine Übersetzungsschicht dazwischen. Sie spricht nach innen die Sprache des eigenen Modells und nach außen die des Fremdsystems. So „verseuchen“ dessen Begriffe, Codes und Eigenheiten nicht das eigene Modell.
Das Problem
Das alte ERP liefert Kunden als Maps mit Schlüsseln wie KDNR und NAM1, Statuscodes wie "A" und "G", Beträge als Text mit Komma und Fehler als „Rückgabecode“. Übernimmt der eigene Code diese Begriffe, breiten sie sich überall aus: Plötzlich prüft der Shop auf STAT == "A", und ein späterer Wechsel des ERP betrifft das ganze System.
Im Code
Die Schnittstelle KundenQuelle ist in der eigenen Sprache formuliert. Der ErpKundenAdapter ist die einzige Klasse, die das ERP kennt. Er übersetzt Kundennummern, Namen, Statuscodes, Beträge und Länder in saubere Typen und den Rückgabecode in ein leeres Optional. Unbekannte Codes führen zu einem klaren Fehler, statt still falsche Daten zu liefern. Die Methode darfAufRechnungKaufen() arbeitet nur mit dem eigenen Modell.
Java-21-Code: AntiCorruptionLayer.java
Direkt ausführbar mit java AntiCorruptionLayer.java
// Anti-Corruption Layer
// Eine Übersetzungsschicht schützt das eigene Modell vor den Begriffen und Eigenheiten eines fremden Systems.
// Start: java AntiCorruptionLayer.java
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class AntiCorruptionLayer {
// ============ Fremdsystem: altes ERP, nicht änderbar ============
static final class AltesErp {
// liefert Datensätze als Map mit kryptischen Schlüsseln, Codes und Zahlen als Text
Map<String, String> KDSATZ_LESEN(String kdnr) {
return switch (kdnr) {
case "0000815" -> Map.of("KDNR", "0000815", "NAM1", "BERG", "NAM2", "SVEN", "STAT", "A", "KRLIM", "5000,00", "LAND", "D");
case "0000816" -> Map.of("KDNR", "0000816", "NAM1", "LENZ", "NAM2", "ANNA", "STAT", "G", "KRLIM", "0,00", "LAND", "A");
default -> Map.of("RC", "4711"); // "Rückgabecode" statt Fehler
};
}
}
// ============ Eigenes, sauberes Domänenmodell ============
record KundenNr(int wert) {}
enum Kundenstatus { AKTIV, GESPERRT }
enum Land { DE, AT, CH }
record Kunde(KundenNr nr, String name, Kundenstatus status, BigDecimal kreditlimit, Land land) {
boolean darfAufRechnungKaufen() { return status == Kundenstatus.AKTIV && kreditlimit.signum() > 0; }
}
interface KundenQuelle { Optional<Kunde> kunde(KundenNr nr); } // Schnittstelle in UNSERER Sprache
// ============ Anti-Corruption Layer: einzige Stelle, die das ERP kennt ============
static final class ErpKundenAdapter implements KundenQuelle {
private final AltesErp erp;
ErpKundenAdapter(AltesErp erp) { this.erp = erp; }
public Optional<Kunde> kunde(KundenNr nr) {
Map<String, String> satz = erp.KDSATZ_LESEN("%07d".formatted(nr.wert()));
if (satz.containsKey("RC")) return Optional.empty();
return Optional.of(new Kunde(
new KundenNr(Integer.parseInt(satz.get("KDNR"))),
schoen(satz.get("NAM2")) + " " + schoen(satz.get("NAM1")),
switch (satz.get("STAT")) {
case "A" -> Kundenstatus.AKTIV;
case "G" -> Kundenstatus.GESPERRT;
default -> throw new IllegalStateException("unbekannter ERP-Status " + satz.get("STAT"));
},
new BigDecimal(satz.get("KRLIM").replace(",", ".")),
switch (satz.get("LAND")) {
case "D" -> Land.DE;
case "A" -> Land.AT;
case "CH" -> Land.CH;
default -> throw new IllegalStateException("unbekanntes ERP-Land " + satz.get("LAND"));
}));
}
private static String schoen(String s) { return s.charAt(0) + s.substring(1).toLowerCase(); }
}
public static void main(String[] args) {
KundenQuelle quelle = new ErpKundenAdapter(new AltesErp());
// Der eigene Code arbeitet nur mit dem eigenen Modell
for (int nr : List.of(815, 816, 999)) {
quelle.kunde(new KundenNr(nr)).ifPresentOrElse(
k -> System.out.printf(java.util.Locale.GERMANY, "%-10s %-9s %-3s Limit %,9.2f € Rechnungskauf: %s%n",
k.name(), k.status(), k.land(), k.kreditlimit(), k.darfAufRechnungKaufen() ? "ja" : "nein"),
() -> System.out.println("Kunde " + nr + " nicht gefunden"));
}
System.out.println("\nRohdaten aus dem ERP zum Vergleich: " + new java.util.TreeMap<>(new AltesErp().KDSATZ_LESEN("0000815")));
}
}Ausgabe
Sven Berg AKTIV DE Limit 5.000,00 € Rechnungskauf: ja
Anna Lenz GESPERRT AT Limit 0,00 € Rechnungskauf: nein
Kunde 999 nicht gefunden
Rohdaten aus dem ERP zum Vergleich: {KDNR=0000815, KRLIM=5000,00, LAND=D, NAM1=BERG, NAM2=SVEN, STAT=A}Worauf achten
Ein Anti-Corruption Layer besteht meist aus mehreren GoF-Mustern: einer Facade, die die komplizierte Fremdschnittstelle vereinfacht, Adaptern, die sie an die eigenen Schnittstellen anpassen, und Translatoren für die Daten. Er kostet Aufwand und lohnt sich, wenn das Fremdsystem ein schlechtes oder ganz anderes Modell hat oder später ausgetauscht werden soll. Ist das fremde Modell gut und stabil, kann man es auch bewusst übernehmen. Das DDD nennt das Conformist.
In der Praxis
Anbindung von SAP und anderen ERP-Systemen, Integration externer Zahlungs- und Versanddienstleister, schrittweise Ablösung von Altsystemen zusammen mit dem Strangler Fig-Muster (Gruppe 7). Auf Nachrichtenebene leisten Message Translator und Normalizer aus den Enterprise Integration Patterns dasselbe.
Die DDD-Bausteine im Zusammenhang
| Baustein | Kurz gesagt | Beispiel | Seite |
|---|---|---|---|
| Value Object | definiert durch Werte, unveränderlich | Geld, Adresse | Gruppe 2 |
| Entity | definiert durch Identität | Kunde, Konto | diese Seite |
| Aggregate | Gruppe mit Wurzel und Regeln | Bestellung mit Positionen | diese Seite |
| Repository | speichert und lädt ganze Aggregate | BestellRepository | Gruppe 3 |
| Domain Event | „Das ist fachlich passiert“ | BestellungAufgegeben | diese Seite |
| Domain Service | Fachlogik ohne eigene Entity | Überweisung | diese Seite |
| Specification | kombinierbare Geschäftsregel | Premiumkunde | Gruppe 2 |
| Anti-Corruption Layer | Schutz vor fremden Modellen | ERP-Adapter | diese Seite |
Neben diesen taktischen Bausteinen beschreibt DDD auch strategische Konzepte wie Bounded Context (abgegrenzte Bereiche mit eigenem Modell) und Context Map (wie diese Bereiche zusammenhängen). Der Anti-Corruption Layer gehört eigentlich schon dazu: Er schützt die Grenze eines Bounded Context.