Durchgehendes Praxisprojekt: Order & Billing Platform

Ein durchgehendes Enterprise-Beispielsystem, das die Theorie der vorherigen Kapitel in einen praxisnahen Architektur- und Codezusammenhang bringt.

V3PraxisprojektSpring/Jakarta/QuarkusMaven Multi-Module
V3-Gesamtlandschaft der Order & Billing Platform
V3-Gesamtlandschaft der Order & Billing Platform

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.

Merksatz: Frameworks betreiben die Anwendung, aber die fachliche Mitte erklärt, warum es die Anwendung gibt.

Fachlicher Schnitt

Bounded ContextVerantwortungNicht-Verantwortung
OrderBestellung anlegen, freigeben, stornierenRechnung berechnen
BillingRechnung und ZahlungsstatusBestellentscheidung
InventoryReservierung und BestandKundenbonität
NotificationBenachrichtigungFachliche 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

  1. REST Controller nimmt PlaceOrderRequest an.
  2. Use Case validiert Command und lädt benötigte Stammdaten.
  3. Aggregate Order schützt Invarianten.
  4. Repository speichert Order.
  5. Outbox speichert OrderPlaced in derselben Transaktion.
  6. Outbox Relay publiziert das Event später zuverlässig.

Code-Slice

Use Case mit Repository, Port, Unit of Work und Outbox
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

FehlerWarum problematischBessere Lösung
Controller enthält GeschäftslogikTests werden langsam und technische Details dominierenController nur als Adapter verwenden
Event direkt nach DB-Save sendenBei Crash nach Commit geht Event verlorenTransactional Outbox
Framework-Anmerkungen in DomainDomain wird schwer testbar und schwer portierbarDomain ohne Framework halten
⌂ Cockpit