Domain Modeling, Use Cases & Invarianten
Dieses Kapitel zeigt, wie aus Fachlichkeit belastbare Java-Modelle entstehen: Value Objects, Aggregates, Policies, Events und Commands.
Fachbegriffe
Der größte Fehler in Enterprise-Projekten ist oft nicht technisch, sondern sprachlich: Kunde, Auftrag, Bestellung, Rechnung, Reservierung und Zahlung werden in jedem Team anders verstanden. Ein Domain Model zwingt das Team zu präzisen Begriffen.
Invarianten
| Invariante | Beispiel | Ort im Code |
|---|---|---|
| Eine Bestellung braucht mindestens eine Position | Leere Bestellung ist ungültig | Order.place |
| Menge muss positiv sein | quantity > 0 | OrderLine.of |
| Stornierte Order darf nicht fakturiert werden | Statuswechsel blockieren | Order.cancel / BillingPolicy |
| Event beschreibt fachliche Tatsache | OrderPlaced nach erfolgreicher Erzeugung | DomainEvent |
Value Objects
Value Objects reduzieren Primitive Obsession. Statt überall String customerId und BigDecimal amount zu verwenden, entstehen Typen mit Regeln.
public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount == null || currency == null) throw new IllegalArgumentException("Money requires amount and currency");
if (amount.scale() > 2) throw new IllegalArgumentException("Money supports cents only");
}
public Money plus(Money other) {
if (!currency.equals(other.currency)) throw new IllegalArgumentException("Currency mismatch");
return new Money(amount.add(other.amount), currency);
}
}
Aggregate
Ein Aggregate ist keine technische Entity-Sammlung. Es ist eine fachliche Konsistenzgrenze. Im Beispiel darf nur Order entscheiden, ob Positionen, Status und Events zusammen gültig sind.
Events
Ein Domain Event beschreibt, was fachlich passiert ist. Es ist nicht primär ein technischer Kafka-Record, sondern eine Wahrheit aus der Fachlichkeit.
Beispiel
public final class Order {
private final OrderId id;
private final CustomerId customerId;
private final List<OrderLine> lines = new ArrayList<>();
private final List<DomainEvent> events = new ArrayList<>();
private OrderStatus status;
private Order(OrderId id, CustomerId customerId) {
this.id = id;
this.customerId = customerId;
this.status = OrderStatus.NEW;
}
// Pattern: Factory Method - erzeugt nur fachlich gültige Orders.
public static Order place(CustomerId customerId, List<OrderLine> requestedLines) {
if (requestedLines == null || requestedLines.isEmpty()) {
throw new DomainRuleViolation("Eine Bestellung braucht mindestens eine Position");
}
Order order = new Order(OrderId.newId(), customerId);
requestedLines.forEach(order::addLine);
order.events.add(new OrderPlaced(order.id, customerId));
return order;
}
public void cancel(String reason) {
if (status == OrderStatus.INVOICED) {
throw new DomainRuleViolation("Fakturierte Bestellung kann nicht storniert werden");
}
status = OrderStatus.CANCELLED;
events.add(new OrderCancelled(id, reason));
}
}