Durchgehendes Praxisprojekt: Order & Billing Platform
Ein durchgehendes Enterprise-Beispielsystem, das die Theorie der vorherigen Kapitel in einen praxisnahen Architektur- und Codezusammenhang bringt.
Zielbild
Das Praxisprojekt zeigt eine realistische Enterprise-Landschaft: Ein Kundenportal erzeugt Bestellungen, die Order-Domain prüft Invarianten, Inventory reserviert Bestand, Billing erzeugt Rechnungen und ein Event Backbone verteilt fachliche Ereignisse. Wichtig ist nicht, möglichst viele Technologien zu verwenden, sondern die Grenzen sauber sichtbar zu machen.
Fachlicher Schnitt
| Bounded Context | Verantwortung | Nicht-Verantwortung |
|---|---|---|
| Order | Bestellung anlegen, freigeben, stornieren | Rechnung berechnen |
| Billing | Rechnung und Zahlungsstatus | Bestellentscheidung |
| Inventory | Reservierung und Bestand | Kundenbonität |
| Notification | Benachrichtigung | Fachliche Entscheidung |
Module
shared-kernel
Gemeinsame primitive Fachtypen, Result-Typ und DomainEvent.
order-domain
Aggregate, Value Objects, Policies und Events ohne Framework-Abhängigkeit.
order-application
Use Cases und Ports. Hier entsteht der fachliche Prozess.
order-adapters
Repository, Outbox, Legacy-Adapter, Billing-Adapter.
spring-boot-app
HTTP- und Betriebsschicht für Spring Boot.
quarkus-app
Cloud-native Variante für Quarkus/MicroProfile.
docs
ADRs, Pattern-Dokumentation, Teststrategie, Runbooks.
Bestellablauf
- REST Controller nimmt
PlaceOrderRequestan. - Use Case validiert Command und lädt benötigte Stammdaten.
- Aggregate
Orderschützt Invarianten. - Repository speichert Order.
- Outbox speichert
OrderPlacedin derselben Transaktion. - Outbox Relay publiziert das Event später zuverlässig.
Code-Slice
public final class PlaceOrderUseCase {
private final OrderRepository orders; // Pattern: Repository Port
private final InventoryPort inventory; // Pattern: Port
private final TransactionRunner tx; // Pattern: Unit of Work Boundary
private final OutboxPort outbox; // Pattern: Transactional Outbox
public PlaceOrderUseCase(OrderRepository orders, InventoryPort inventory,
TransactionRunner tx, OutboxPort outbox) {
this.orders = orders;
this.inventory = inventory;
this.tx = tx;
this.outbox = outbox;
}
public OrderId handle(PlaceOrderCommand command) {
return tx.required(() -> {
inventory.reserve(command.customerId(), command.lines());
Order order = Order.place(command.customerId(), command.lines()); // Factory Method
orders.save(order);
outbox.appendAll(order.pullEvents()); // Outbox: Event wird mit Order atomar gespeichert
return order.id();
});
}
}
Typische Stolperfallen
| Fehler | Warum problematisch | Bessere Lösung |
|---|---|---|
| Controller enthält Geschäftslogik | Tests werden langsam und technische Details dominieren | Controller nur als Adapter verwenden |
| Event direkt nach DB-Save senden | Bei Crash nach Commit geht Event verloren | Transactional Outbox |
| Framework-Anmerkungen in Domain | Domain wird schwer testbar und schwer portierbar | Domain ohne Framework halten |