Praxis Edition · Struktur 2.0 · Enterprise Ready

Clean Code & Java Design Lernpfad

Neu strukturiert als klarer Lernpfad: Orientierung, Clean Java Foundations, Domain Design, Effective Java, Patterns, Enterprise Architecture, Refactoring und Portfolio.

Struktur 2.0Sidebar-SucheKlappbare KapitelDark/LightSpring/Jakarta/Plain JavaEnterprise Code
Struktur-Update: Das HTML-Verzeichnis bleibt außerhalb des Bodys. Die Sidebar ist die Navigation; der Hauptbereich zeigt den Lernpfad in Phasen.
Struktur 2.0: Lernphasen
SetupPhase 0CleanJavaDomainOOPEffectiveJavaPatterns& Arch.EnterpriseRefactoring
Enterprise Boundary
APIDTOsApplicationUse CaseDomainAggregateRepositoryPayment
Refactoring Loop
SmellfindenTestsichernkleinerSchrittgrüncommit

Clean Code & Java Design Lernpfad – Praxis Edition

Ziel: In 16 Wochen von „ich schreibe funktionierenden Java-Code“ zu „ich entwerfe wartbare, testbare und enterprise-taugliche Systeme“ wachsen.
Praxisprojekt: OrderFlow, ein kleines Bestell-Backend mit Kunden, Produkten, Bestellungen, Zahlung, Rechnung und Events.
Fokus: Clean Code, Effective Java, klassische Entwurfsmuster, Enterprise Patterns, Antipatterns, Code Smells und Refactoring.
Format: Theorie → Bild/Diagramm → schlechtes Beispiel → gutes Beispiel → Übung → Review-Checkliste.
Empfohlener Java-Stand: JDK 25 LTS für neue Projekte; JDK 21 LTS bleibt ein sehr verbreiteter Enterprise-Basisstand.


Update UI: Die HTML-Version besitzt klappbare Hauptkapitel, eine klappbare Sidebar-Navigation ohne zusätzliches Body-Verzeichnis, Live-Suche, Buttons für „Alle öffnen/schließen“, Code-Tabs für Spring/Jakarta/Plain Java, speicherbaren Lernfortschritt, persistente Checklisten, „Variante komplett kopieren“-Buttons und einen Dark/Light-Switch. Die Struktur wurde auf Lernphasen umgestellt: Orientierung → Foundations → Domain/OOP → Effective Java → Patterns → Enterprise → Refactoring → Portfolio.


Lernstruktur 2.0

Die neue Struktur ist stärker lern- und projektorientiert. Statt viele Themen nebeneinander zu stellen, führt sie dich in klaren Phasen durch denselben Enterprise-Use-Case: von lesbarem Java-Code bis zu Architektur, Refactoring und Abschlussprojekt.

flowchart LR
    P0[Phase 0\nOrientierung & Setup] --> P1[Phase 1\nClean Java Foundations]
    P1 --> P2[Phase 2\nDomain & OOP]
    P2 --> P3[Phase 3\nEffective Java]
    P3 --> P4[Phase 4\nPatterns entscheiden]
    P4 --> P5[Phase 5\nEnterprise Architecture]
    P5 --> P6[Phase 6\nEnterprise Codepack]
    P6 --> P7[Phase 7\nAntipatterns]
    P7 --> P8[Phase 8\nRefactoring]
    P8 --> P9[Phase 9\nPraxis, Reviews, Portfolio]
Phase Fokus Ergebnis
0 Orientierung, Setup, Repository-Regeln du weißt, wie du den Kurs nutzt
1 Namen, Funktionen, Fehlergrenzen, Tests lesbarer Java-Code
2 OOP, Domain Model, Value Objects fachlich geschützte Objekte
3 Effective Java in echten Use Cases robuste Java-Idiome
4 Entwurfsmuster nach Entscheidungslage Pattern bewusst statt reflexartig
5 Enterprise Patterns und Architekturgrenzen saubere Schichten und Ports
6 Enterprise-ready OrderFlow-Code realistische Spring/Jakarta/Plain-Java-Varianten
7 Antipatterns und Code Smells Diagnosefähigkeit
8 Legacy-Refactoring kontrollierte Transformation
9 Übungen, Reviews, Abschlussprojekt portfoliofähiger Nachweis

Didaktische Reihenfolge pro Phase: Lernziel → mentales Modell → schlechtes Beispiel → bessere Variante → Enterprise-Bezug → Übung → Review-Kriterien.


Phase 0: Orientierung, Setup und Kursregeln

Dateiverzeichnis

clean_code_java_lernpfad/
├── README.md        # Markdown-Lernpfad mit Mermaid-Diagrammen
└── index.html       # Eigenständige HTML-Version mit klappbarer Sidebar, Suche, Code-Tabs, Lernfortschritt und Dark/Light-Switch

Package-Konvention

Alle Beispiele folgen derselben Namensregel:

Variante Package-Wurzel Beispiel
Spring com.seb4u.demo.spring... com.seb4u.demo.spring.application
Jakarta com.seb4u.demo.jakarta... com.seb4u.demo.jakarta.application
Framework-neutral com.seb4u.demo... com.seb4u.demo.domain

Empfohlene Repository-Struktur für die Übungen:

orderflow-clean-code/
├── README.md
├── docs/
│   ├── architecture.md
│   ├── decision-log.md
│   ├── code-review-checklist.md
│   └── refactoring-journal.md
├── src/main/java/com/seb4u/demo/spring/
│   ├── api/
│   ├── application/
│   ├── domain/
│   ├── infrastructure/
│   └── shared/
├── src/test/java/com/seb4u/demo/spring/
│   ├── architecture/
│   ├── application/
│   ├── domain/
│   └── refactoring/
├── katas/
│   ├── naming-and-functions/
│   ├── builder-and-immutability/
│   ├── strategy-discounts/
│   ├── repository-and-uow/
│   ├── outbox-events/
│   └── legacy-order-service/
└── reviews/
    ├── week-01.md
    ├── week-02.md
    └── final-review.md

So nutzt du diesen Lernpfad

Jede Etappe folgt demselben Muster:

Baustein Was du tust Ergebnis
Ziel Du verstehst, welche Fähigkeit aufgebaut wird klares Lernziel
Theorie Du lernst die wichtigsten Prinzipien Entscheidungsgrundlage
Bild/Diagramm Du siehst Zusammenhänge visuell schneller Überblick
schlechtes Beispiel Du erkennst echte Probleme Smell-Erkennung
gutes Beispiel Du siehst eine bessere Lösung wiederverwendbares Muster
Übung Du setzt es am Projekt um sichtbarer Fortschritt
Review Du prüfst Qualität systematisch belastbare Gewohnheit

Arbeitsrhythmus pro Woche:

  1. 60–90 Minuten lesen und Beispiele nachvollziehen.
  2. 2–4 Stunden im Projekt OrderFlow umsetzen.
  3. 60 Minuten Tests schreiben oder verbessern.
  4. 45 Minuten Review mit Checkliste.
  5. 15 Minuten Refactoring-Journal aktualisieren.

Regel: Jede Woche endet mit einem Commit, einem kurzen Architekturentscheid und einer Review-Notiz.


Visuelle Gesamtkarte

mindmap
  root((Java Software Craft))
    Clean Code
      Namen
      Kleine Funktionen
      Fehlerbehandlung
      Tests
      Grenzen
    OOP Design
      Kapselung
      Kohäsion
      Kopplung
      Polymorphie
      SOLID
    Effective Java
      Static Factories
      Builder
      Immutability
      Optional
      Exceptions
      Generics
      Enums
      Concurrency
    Design Patterns
      Creational
      Structural
      Behavioral
      Entscheidung statt Auswendiglernen
    Enterprise Patterns
      Service Layer
      Repository
      Unit of Work
      DTO
      Outbox
      Saga
      Circuit Breaker
    Antipatterns
      God Service
      Anemic Domain
      Big Ball of Mud
      Golden Hammer
      Distributed Monolith
    Refactoring
      Tests sichern
      Kleine Schritte
      Smells erkennen
      Design verbessern

Lernfluss

flowchart LR
    A[Lesbarkeit] --> B[Objektdesign]
    B --> C[Effective Java]
    C --> D[Design Patterns]
    D --> E[Enterprise Patterns]
    E --> F[Antipatterns erkennen]
    F --> G[Refactoring]
    G --> H[Saubere Architektur]
    H --> B

Merksatz: Clean Code beginnt bei Namen und Methoden, wird aber erst durch gutes Design, Tests und Refactoring dauerhaft stabil.


Praxisprojekt OrderFlow

Fachlicher Kontext

OrderFlow ist ein kleines Bestellsystem:

  • Kundinnen und Kunden können Produkte bestellen.
  • Eine Bestellung besteht aus Positionen, Preisen, Rabatten und Status.
  • Zahlung und Rechnung sind zunächst simuliert.
  • Später werden Events, Outbox, Transaktionen und externe Payment-Provider ergänzt.
flowchart TB
    Customer[Kunde] --> Order[Bestellung]
    Order --> OrderLine[Bestellposition]
    OrderLine --> Product[Produkt]
    Order --> Discount[Rabattregel]
    Order --> Payment[Zahlung]
    Order --> Invoice[Rechnung]
    Order --> Event[Domain Event]

Zielarchitektur

flowchart TB
    API[API / Controller] --> APP[Application Service]
    APP --> DOM[Domain Model]
    APP --> PORTS[Ports]
    PORTS --> REPO[Repository Interface]
    PORTS --> PAY[Payment Port]
    REPO --> DB[(Database)]
    PAY --> EXT[External Payment Provider]
    DOM --> EVENTS[Domain Events]
    APP --> OUTBOX[Outbox]

Grobe Modulstruktur

Modul Verantwortung Darf abhängig sein von
domain Regeln, Entitäten, Value Objects, Domain Events möglichst nichts Technisches
application Use Cases, Transaktionen, Orchestrierung Domain, Ports
api HTTP/REST, Request/Response DTOs, Validierung Application
infrastructure Datenbank, externe APIs, Messaging Application Ports, Domain
shared kleine technische Hilfen sehr sparsam einsetzen

OrderFlow-Meilensteine

Woche Meilenstein Lernschwerpunkt
1 Projekt-Skeleton, Tests, Formatter Arbeitsweise
2 saubere Namen und kleine Methoden Clean Code
3 Domain-Grundmodell OOP, Kapselung
4 Value Objects und Invarianten Immutability
5 Builder und Factories Effective Java
6 Exceptions und Optional sauber einsetzen Fehlerbehandlung
7 Strategy für Rabatte Behavioral Patterns
8 Factory/Adapter für Payment Creational/Structural Patterns
9 Observer/Event für Bestellereignisse Event-Denken
10 Repository und Service Layer Enterprise Patterns
11 DTO/Mapper und Transaktionen Boundaries
12 Outbox und idempotente Verarbeitung Reliability
13 Antipatterns bewusst einbauen und erkennen Diagnose
14 Legacy-Refactoring des OrderService Transformation
15 Tests, Architekturregeln, Review Absicherung
16 Abschlussprojekt und Portfolio Nachweis

16-Wochen-Roadmap nach Phasen

Woche Thema Ergebnis Commit-Idee
1 Setup, Tests, Formatierung lauffähiges Projekt init-orderflow-project
2 Namen, Funktionen, Kommentare lesbarere Services clean-naming-and-functions
3 Kohäsion, Kopplung, Kapselung erstes Domain-Modell introduce-order-domain
4 Value Objects, Invarianten robustere Daten add-money-and-quantity-value-objects
5 Builder, Static Factories saubere Objekterzeugung add-order-builder
6 Optional, Exceptions, Ressourcen klare Fehlergrenzen improve-error-handling
7 Strategy austauschbare Rabatte add-discount-strategies
8 Factory, Adapter Payment-Integration kapseln add-payment-adapter
9 Observer, Command, State Ereignisse und Status add-domain-events-and-state
10 Service Layer, Repository Enterprise-Schichten add-service-layer-and-repositories
11 DTO, Mapper, Transaktionen API/Domain getrennt separate-dto-and-domain
12 Outbox, Saga, Circuit Breaker verlässlichere Integration add-outbox-pattern
13 Antipatterns Diagnosekatalog document-code-smells
14 Refactoring-Techniken Legacy-Code verbessern refactor-legacy-order-service
15 Architekturtests, Review Qualität absichern add-architecture-tests
16 Abschlussprojekt Portfolio-fähiges Ergebnis final-orderflow-release

Phase 1: Clean Java Foundations

Ziel

Du schreibst Java-Code, der für andere Menschen leicht lesbar, testbar und änderbar ist.

Kernideen

  • Namen sollen Absicht ausdrücken, nicht Implementierungsdetails verstecken.
  • Funktionen sollen eine Aufgabe erfüllen und auf einer Abstraktionsebene bleiben.
  • Kommentare erklären Warum, nicht Was.
  • Fehlerbehandlung gehört in klare Grenzen.
  • Tests sind Sicherheitsnetz, Dokumentation und Designfeedback.

Bildlicher Input: Methode als Pipeline

flowchart LR
    A[Eingabe validieren] --> B[Domain-Objekt erzeugen]
    B --> C[Regel anwenden]
    C --> D[Persistieren]
    D --> E[Antwort erzeugen]

Schlechtes Beispiel

public void doIt(OrderRequest r) {
    if (r != null && r.items() != null && r.items().size() > 0) {
        double t = 0;
        for (var i : r.items()) {
            if (i.price() > 0 && i.qty() > 0) {
                t += i.price() * i.qty();
            }
        }
        if (r.vip()) {
            t = t * 0.9;
        }
        repo.save(new OrderEntity(r.customerId(), t, "NEW"));
    }
}

Probleme:

  • doIt, r, t sagen wenig aus.
  • Validierung, Berechnung und Persistenz sind vermischt.
  • double ist schlecht für Geldbeträge.
  • Status als String ist fehleranfällig.
  • Keine klaren Fehlerfälle.

Besseres Beispiel

public OrderId placeOrder(PlaceOrderCommand command) {
    var draft = orderDraftFactory.from(command);
    var pricedOrder = pricingService.price(draft);
    var order = Order.place(pricedOrder);
    orderRepository.save(order);
    return order.id();
}

Die Details sind nicht verschwunden, sondern an benannte Stellen verschoben.

Übung in OrderFlow

  1. Erstelle PlaceOrderCommand.
  2. Zerlege eine lange Methode in 4–6 sprechende Methoden oder Klassen.
  3. Ersetze primitive Geldbeträge durch Money.
  4. Schreibe Tests für Preisberechnung und leere Warenkörbe.
  5. Dokumentiere im Refactoring-Journal: „Was wurde leichter zu verstehen?“

Review-Checkliste


Phase 2: Objektorientiertes Domain Design

Ziel

Du modellierst Verhalten dort, wo die fachlichen Daten leben, und reduzierst unnötige Kopplung.

Kernideen

Konzept Gute Frage
Kohäsion Gehört dieses Verhalten wirklich in diese Klasse?
Kopplung Wie viele andere Klassen müssen sich ändern, wenn ich diese ändere?
Kapselung Schützt das Objekt seine Invarianten?
Polymorphie Kann ich if/else durch austauschbares Verhalten ersetzen?
Komposition Kann ich Verhalten zusammensetzen statt vererben?

Bildlicher Input: Datenmodell vs. Verhaltensmodell

flowchart LR
    A[Anemic Model\nDaten + Setter] --> B[Service macht alles]
    C[Rich Domain Model\nDaten + Regeln] --> D[Service orchestriert]

Schlechtes Beispiel: Anemic Domain Model

public class Order {
    public List<OrderLine> lines;
    public String status;
    public BigDecimal total;
}

public class OrderService {
    public void cancel(Order order) {
        if (order.status.equals("SHIPPED")) {
            throw new IllegalStateException("too late");
        }
        order.status = "CANCELLED";
    }
}

Besseres Beispiel: Objekt schützt Regel

public final class Order {
    private final OrderId id;
    private final List<OrderLine> lines;
    private OrderStatus status;

    public void cancel() {
        if (status == OrderStatus.SHIPPED) {
            throw new OrderAlreadyShippedException(id);
        }
        status = OrderStatus.CANCELLED;
    }
}

Übung in OrderFlow

  1. Modelliert Order, OrderLine, Money, Quantity, OrderStatus.
  2. Verschiebt fachliche Regeln aus Services in Domain-Objekte.
  3. Macht Collections nach außen unveränderbar.
  4. Ersetzt Status-Strings durch enum oder State Pattern.

Review-Checkliste


Phase 3: Effective Java Toolbox

Ziel

Du verwendest moderne und robuste Java-Techniken bewusst: Factories, Builder, Immutability, Generics, Optional, Exceptions, Enums, Records und Concurrency.

3.1 Static Factory Methods

Wann? Wenn Konstruktoren nicht genug Absicht ausdrücken.

public final class Money {
    private final BigDecimal amount;
    private final Currency currency;

    private Money(BigDecimal amount, Currency currency) {
        if (amount.scale() > 2) {
            throw new IllegalArgumentException("Money supports max 2 decimals");
        }
        this.amount = amount;
        this.currency = Objects.requireNonNull(currency);
    }

    public static Money eur(String amount) {
        return new Money(new BigDecimal(amount), Currency.getInstance("EUR"));
    }

    public static Money of(BigDecimal amount, Currency currency) {
        return new Money(amount, currency);
    }
}

3.2 Builder

Wann? Wenn ein Objekt viele optionale Parameter hat oder seine Erzeugung lesbar bleiben soll.

Order order = Order.builder()
    .customerId(customerId)
    .addLine(productId, Quantity.of(2), Money.eur("19.90"))
    .discountCode("SUMMER10")
    .build();

3.3 Records und Immutability

Records eignen sich gut für einfache, transparente Datenobjekte, etwa Commands, DTOs und Value Objects. Sie sind kein Ersatz für jedes Domain-Objekt, besonders dann nicht, wenn Identität, Lebenszyklus oder komplexes Verhalten wichtig sind.

public record PlaceOrderCommand(
    CustomerId customerId,
    List<PlaceOrderLine> lines,
    Optional<String> discountCode
) {
    public PlaceOrderCommand {
        Objects.requireNonNull(customerId);
        lines = List.copyOf(lines);
        if (lines.isEmpty()) {
            throw new IllegalArgumentException("Order must contain at least one line");
        }
    }
}

3.4 Optional richtig einsetzen

Gute Anwendung:

public Optional<Customer> findByEmail(Email email) {
    return customerRepository.findByEmail(email);
}

Schwache Anwendung:

public void sendInvoice(Optional<Order> order) { // nicht als Parameter erzwingen
    order.ifPresent(this::sendInvoice);
}

Faustregel: Optional bevorzugt als Rückgabetyp verwenden, wenn „kein Ergebnis“ ein normaler Fall ist.

3.5 Exceptions

public final class OrderAlreadyShippedException extends RuntimeException {
    public OrderAlreadyShippedException(OrderId id) {
        super("Order " + id.value() + " is already shipped and cannot be cancelled");
    }
}

Gute Exceptions sind fachlich, konkret und an Grenzen übersetzbar, zum Beispiel in HTTP 409 Conflict.

3.6 try-with-resources

try (var stream = Files.lines(path)) {
    return stream.filter(line -> !line.isBlank()).toList();
}

3.7 Enums mit Verhalten

public enum OrderStatus {
    NEW {
        @Override boolean canCancel() { return true; }
    },
    PAID {
        @Override boolean canCancel() { return true; }
    },
    SHIPPED {
        @Override boolean canCancel() { return false; }
    };

    abstract boolean canCancel();
}

3.8 Concurrency und Virtual Threads

Virtual Threads passen gut zu vielen blockierenden I/O-Aufgaben, zum Beispiel viele unabhängige externe HTTP-Aufrufe. Sie ersetzen aber kein sauberes Ressourcen-, Timeout- und Backpressure-Design.

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    var payment = executor.submit(() -> paymentClient.authorize(order));
    var inventory = executor.submit(() -> inventoryClient.reserve(order));

    return new CheckoutResult(payment.get(), inventory.get());
}

Übung in OrderFlow

  • Erstelle Money, Quantity, Email als Value Objects.
  • Nutze Records für Commands und DTOs.
  • Baue einen OrderBuilder.
  • Ersetze primitive Status- und Typfelder.
  • Schreibe Tests für Ungültigkeiten.

Phase 4: Entwurfsmuster entscheidungsbasiert

Ziel

Du erkennst, welches Muster zu welchem Problem passt. Patterns sind kein Dekorationsmittel, sondern eine Sprache für wiederkehrende Designentscheidungen.

Pattern-Gruppen

Gruppe Zweck Beispiele in OrderFlow
Creational Objekterzeugung kapseln Builder, Factory Method, Abstract Factory
Structural Objekte flexibel verbinden Adapter, Decorator, Facade
Behavioral Verhalten austauschbar machen Strategy, Observer, Command, State, Template Method

Strategy: Rabattregeln

public interface DiscountPolicy {
    Money apply(OrderDraft order);
}

public final class VipDiscountPolicy implements DiscountPolicy {
    public Money apply(OrderDraft order) {
        return order.subtotal().multiply("0.10");
    }
}

public final class NoDiscountPolicy implements DiscountPolicy {
    public Money apply(OrderDraft order) {
        return Money.zero(order.currency());
    }
}

Anwendung:

public PricedOrder price(OrderDraft draft, DiscountPolicy discountPolicy) {
    Money discount = discountPolicy.apply(draft);
    return new PricedOrder(draft, draft.subtotal().minus(discount));
}

Factory: Payment Provider auswählen

public final class PaymentProviderFactory {
    public PaymentProvider providerFor(PaymentMethod method) {
        return switch (method) {
            case CARD -> new CardPaymentProvider();
            case PAYPAL -> new PaypalPaymentProvider();
            case INVOICE -> new InvoicePaymentProvider();
        };
    }
}

Adapter: Externe API kapseln

public final class StripePaymentAdapter implements PaymentPort {
    private final StripeClient client;

    public PaymentResult authorize(PaymentRequest request) {
        StripeResponse response = client.charge(toStripeRequest(request));
        return toPaymentResult(response);
    }
}

Observer / Domain Events

public record OrderPlaced(OrderId orderId, CustomerId customerId) implements DomainEvent {}

public final class Order {
    private final List<DomainEvent> events = new ArrayList<>();

    public static Order place(PricedOrder pricedOrder) {
        Order order = new Order(pricedOrder);
        order.events.add(new OrderPlaced(order.id, order.customerId));
        return order;
    }
}

State: Bestellstatus

stateDiagram-v2
    [*] --> NEW
    NEW --> PAID: payment accepted
    NEW --> CANCELLED: cancel
    PAID --> SHIPPED: ship
    PAID --> REFUNDED: refund
    SHIPPED --> DELIVERED: deliver

Übung in OrderFlow

  1. Baue DiscountPolicy als Strategy.
  2. Kapsle Payment mit Adapter.
  3. Erzeuge Payment-Provider über Factory.
  4. Führe Domain Events für OrderPlaced ein.
  5. Modelliert Bestellstatus mit Enum oder State Pattern.

Pattern-Warnung

Ein Pattern ist gut, wenn es Komplexität sichtbar reduziert. Ein Pattern ist schlecht, wenn es nur Klassenanzahl erhöht und das Problem kleiner ist als die Lösung.


Phase 5: Enterprise Patterns und Architekturgrenzen

Ziel

Du strukturierst Anwendungen so, dass Fachlogik, Infrastruktur, Transaktionen, Integration und API-Grenzen sauber getrennt sind.

Layered Architecture

flowchart TB
    Controller[Controller / REST API] --> Service[Application Service]
    Service --> Domain[Domain Model]
    Service --> Repository[Repository]
    Repository --> Database[(Database)]

Gut für: klare Einstiegspunkte, klassische Enterprise-Anwendungen, Teamverständnis.
Risiko: Schichten werden zu reinen Durchreichstationen.

Hexagonal Architecture

flowchart LR
    REST[REST Adapter] --> APP[Application Core]
    CLI[CLI Adapter] --> APP
    APP --> DBPORT[Repository Port]
    APP --> PAYPORT[Payment Port]
    DBPORT --> JPA[JPA Adapter]
    PAYPORT --> EXT[External Payment]

Gut für: testbare Kernlogik, austauschbare Infrastruktur, klare Boundaries.

Service Layer

public final class PlaceOrderService {
    private final OrderRepository orders;
    private final PricingService pricing;
    private final TransactionRunner tx;

    public OrderId place(PlaceOrderCommand command) {
        return tx.inTransaction(() -> {
            var draft = OrderDraft.from(command);
            var order = Order.place(pricing.price(draft));
            orders.save(order);
            return order.id();
        });
    }
}

Repository

public interface OrderRepository {
    Optional<Order> findById(OrderId id);
    void save(Order order);
}

Das Interface liegt im Kern; die Implementierung liegt in der Infrastruktur.

Unit of Work / Transaktionen

In vielen Java-Enterprise-Anwendungen wird Unit of Work indirekt über ORM und Transaktionsgrenzen realisiert. Wichtig ist nicht der Name, sondern die Regel: Ein Use Case hat eine klare Konsistenzgrenze.

DTO und Mapper

public record PlaceOrderRequest(String customerId, List<LineRequest> lines) {}
public record PlaceOrderResponse(String orderId) {}

DTOs gehören an Grenzen. Domain-Objekte sollten nicht ungefiltert als API-Verträge nach außen gehen.

Outbox Pattern

sequenceDiagram
    participant App
    participant DB
    participant Worker
    participant Broker
    App->>DB: Order + OutboxEvent in derselben Transaktion speichern
    Worker->>DB: unverarbeitete OutboxEvents lesen
    Worker->>Broker: Event publizieren
    Worker->>DB: Event als verarbeitet markieren

Saga

Eine Saga koordiniert mehrere lokale Transaktionen, wenn eine große verteilte Transaktion nicht sinnvoll ist. Beispiel: Bestellung erstellen → Zahlung autorisieren → Lager reservieren → Rechnung erzeugen. Fehlschritte lösen Kompensationen aus.

Circuit Breaker

Ein Circuit Breaker schützt dein System vor wiederholten Aufrufen eines bereits gestörten Fremdsystems.

stateDiagram-v2
    Closed --> Open: viele Fehler
    Open --> HalfOpen: Wartezeit vorbei
    HalfOpen --> Closed: Testaufruf erfolgreich
    HalfOpen --> Open: Testaufruf fehlerhaft

Übung in OrderFlow

  • Trenne Controller, Application Service, Domain und Infrastruktur.
  • Nutze DTOs nur an API-Grenzen.
  • Definiere Ports für Payment und Repository.
  • Baue eine einfache Outbox-Tabelle.
  • Schreibe Tests, die Domain ohne Datenbank testen.

Phase 6: Enterprise-Ready Codepack für OrderFlow

Ziel dieses Upgrades

Die bisherigen Codebeispiele zeigen Prinzipien. Dieses Codepack zeigt dieselben Prinzipien in einer realistischeren Enterprise-Variante: klarere Grenzen, mehr Fehlerfälle, Security-Grenzen, Transaktionen, Idempotenz, Domain Events, Outbox, Ports und Tests.

Wichtig: Enterprise-ready bedeutet nicht „maximal kompliziert“. Es bedeutet: Der Code macht Geschäftsregeln, Änderungsstellen, Fehlerfälle und Infrastrukturgrenzen explizit.

Bildlicher Input: Enterprise Use Case Boundary

flowchart LR
    Client[Client / REST] --> Controller[Controller + DTO Validation]
    Controller --> UseCase[Application Service]
    UseCase --> Auth[Access Policy]
    UseCase --> Domain[Order Aggregate]
    UseCase --> RepoPort[OrderRepository Port]
    UseCase --> PayPort[Payment Port]
    UseCase --> Outbox[Outbox Writer]
    RepoPort --> Jpa[JpaOrderRepository Adapter]
    PayPort --> Provider[Payment Provider Adapter]
    Outbox --> Worker[Outbox Publisher]
    Worker --> Broker[(Message Broker)]

Paketstruktur für enterprise-ready OrderFlow

Package-Konvention: Spring-Boot- und OrderFlow-Beispiele verwenden com.seb4u.demo.spring.... Jakarta-EE/Jakarta-only Beispiele verwenden com.seb4u.demo.jakarta.... Framework-neutrale Java-Beispiele verwenden com.seb4u.demo.... Vermeide generische Demo-Packages; alle vollständigen Java-Dateien bekommen explizit ein package nach dieser Regel.

src/main/java/com/seb4u/demo/spring/
├── api/
│   ├── OrderController.java
│   ├── PlaceOrderRequest.java
│   ├── OrderResponse.java
│   └── ApiExceptionHandler.java
├── application/
│   ├── PlaceOrderCommand.java
│   ├── PlaceOrderUseCase.java
│   ├── PlaceOrderService.java
│   ├── IdempotencyKey.java
│   └── ports/
│       ├── OrderRepository.java
│       ├── PaymentPort.java
│       ├── OutboxWriter.java
│       ├── CustomerAccessPolicy.java
│       └── ProductCatalogPort.java
├── domain/
│   ├── Order.java
│   ├── OrderLine.java
│   ├── OrderStatus.java
│   ├── Money.java
│   ├── CustomerId.java
│   ├── ProductId.java
│   ├── DomainEvent.java
│   └── OrderPlaced.java
├── infrastructure/
│   ├── persistence/
│   │   ├── JpaOrderRepository.java
│   │   ├── SpringDataOrderJpaRepository.java
│   │   └── OrderJpaEntity.java
│   ├── payment/
│   │   └── StripePaymentAdapter.java
│   └── outbox/
│       ├── JdbcOutboxWriter.java
│       └── OutboxPublisherJob.java
└── shared/
    ├── BusinessException.java
    ├── NotFoundException.java
    └── ErrorCode.java

Enterprise-Projektstruktur als Lernmodul

Diese Struktur ist als Lernmodul gedacht: Du kannst jede Schicht einzeln lesen, testen und refactoren. Die wichtigste Regel lautet: Abhängigkeiten zeigen nach innen. Frameworks, Datenbanken und externe Provider bleiben außen.

API            -> DTOs, Validation, Fehler-Mapping
Application    -> Use Case, Transaktion, Idempotenz, Ports
Domain         -> Aggregate, Value Objects, Invarianten, Events
Ports          -> Interfaces zu Repository, Payment, Outbox, Catalog
Adapters       -> JPA, Payment-Provider, Message Broker, Scheduler
Regel Konsequenz für Reviews
Domain kennt kein Spring/Jakarta Keine Framework-Annotationen im Domain-Paket erlauben.
Use Case spricht über Ports Externe Systeme werden in Tests mit Fakes ersetzt.
Adapter implementieren Details JPA, HTTP und Payment-Provider bleiben austauschbar.
Outbox liegt in derselben Transaktion wie Order Events gehen nicht verloren, wenn Publishing später fehlschlägt.

HTML-Extra: Die HTML-Version zeigt diese Struktur als visuelle Modulkarte. Zusätzlich kannst du Hauptkapitel abhaken, Checklisten-Zustände behalten und komplette Code-Tab-Varianten kopieren.

Code-Tabs: Spring, Jakarta, Plain Java

Diese drei Varianten zeigen denselben Enterprise-Use-Case mit derselben Package-Regel. Der Vergleich ist bewusst praxisnah: API-Grenze, Idempotenz, Transaktion, Ports, Domain-Invarianten und Fehlerbehandlung bleiben sichtbar, aber das Framework wechselt.

Variante Package-Wurzel Wann sinnvoll?
Spring com.seb4u.demo.spring... Spring Boot, Spring MVC, Spring Data, Spring Security, @Transactional
Jakarta com.seb4u.demo.jakarta... Jakarta EE, JAX-RS, CDI, JTA, Bean Validation
Plain Java com.seb4u.demo... Domain/Application-Core ohne Framework-Abhängigkeit, schnell testbar

Spring

Datei: src/main/java/com/seb4u/demo/spring/application/PlaceOrderService.java

package com.seb4u.demo.spring.application;

import com.seb4u.demo.spring.application.ports.CustomerAccessPolicy;
import com.seb4u.demo.spring.application.ports.OrderRepository;
import com.seb4u.demo.spring.application.ports.OutboxWriter;
import com.seb4u.demo.spring.application.ports.PaymentPort;
import com.seb4u.demo.spring.application.ports.ProductCatalogPort;
import com.seb4u.demo.spring.domain.DomainEvent;
import com.seb4u.demo.spring.domain.Money;
import com.seb4u.demo.spring.domain.Order;
import com.seb4u.demo.spring.domain.OrderLine;
import com.seb4u.demo.spring.shared.BusinessException;
import java.time.Clock;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public final class PlaceOrderService implements PlaceOrderUseCase {
    private final CustomerAccessPolicy accessPolicy;
    private final ProductCatalogPort productCatalog;
    private final PaymentPort paymentPort;
    private final OrderRepository orderRepository;
    private final OutboxWriter outboxWriter;
    private final Clock clock;

    public PlaceOrderService(
        CustomerAccessPolicy accessPolicy,
        ProductCatalogPort productCatalog,
        PaymentPort paymentPort,
        OrderRepository orderRepository,
        OutboxWriter outboxWriter,
        Clock clock
    ) {
        this.accessPolicy = accessPolicy;
        this.productCatalog = productCatalog;
        this.paymentPort = paymentPort;
        this.orderRepository = orderRepository;
        this.outboxWriter = outboxWriter;
        this.clock = clock;
    }

    @Override
    @Transactional
    public PlaceOrderResult place(PlaceOrderCommand command) {
        accessPolicy.assertCanPlaceOrder(command.actorId(), command.customerId());

        return orderRepository.findByIdempotencyKey(command.idempotencyKey())
            .map(PlaceOrderResult::fromExisting)
            .orElseGet(() -> createNewOrder(command));
    }

    private PlaceOrderResult createNewOrder(PlaceOrderCommand command) {
        List<OrderLine> lines = command.lines().stream()
            .map(line -> productCatalog.requireSellableProduct(
                line.productId(),
                line.quantity(),
                command.currency()
            ))
            .toList();

        Order order = Order.place(
            command.customerId(),
            command.idempotencyKey(),
            lines,
            clock.instant()
        );

        Money amountToAuthorize = order.total();
        PaymentAuthorization authorization = paymentPort.authorize(
            command.customerId(),
            amountToAuthorize,
            command.idempotencyKey().asProviderReference()
        );

        if (!authorization.accepted()) {
            throw BusinessException.paymentRejected(authorization.reason());
        }

        order.markPaymentAuthorized(authorization.authorizationId(), clock.instant());
        Order saved = orderRepository.save(order);

        List<DomainEvent> events = saved.pullDomainEvents();
        outboxWriter.store(events);

        return PlaceOrderResult.fromCreated(saved);
    }
}

Jakarta

Datei: src/main/java/com/seb4u/demo/jakarta/application/PlaceOrderResource.java

package com.seb4u.demo.jakarta.application;

import com.seb4u.demo.jakarta.application.ports.OrderRepository;
import com.seb4u.demo.jakarta.application.ports.OutboxWriter;
import com.seb4u.demo.jakarta.application.ports.PaymentPort;
import com.seb4u.demo.jakarta.application.ports.ProductCatalogPort;
import com.seb4u.demo.jakarta.domain.Money;
import com.seb4u.demo.jakarta.domain.Order;
import com.seb4u.demo.jakarta.domain.OrderLine;
import com.seb4u.demo.jakarta.shared.ProblemDetailsException;
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;
import java.net.URI;
import java.time.Clock;
import java.util.List;

@Path("/orders")
@ApplicationScoped
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class PlaceOrderResource {
    @Inject ProductCatalogPort productCatalog;
    @Inject PaymentPort paymentPort;
    @Inject OrderRepository orderRepository;
    @Inject OutboxWriter outboxWriter;
    @Inject Clock clock;

    @POST
    @RolesAllowed("customer")
    @Transactional
    public Response place(@Valid PlaceOrderRequest request, @Context SecurityContext security) {
        var command = request.toCommand(security.getUserPrincipal().getName());

        var existing = orderRepository.findByIdempotencyKey(command.idempotencyKey());
        if (existing.isPresent()) {
            return Response.ok(OrderResponse.from(existing.get())).build();
        }

        List<OrderLine> lines = command.lines().stream()
            .map(line -> productCatalog.requireSellableProduct(
                line.productId(),
                line.quantity(),
                command.currency()
            ))
            .toList();

        Order order = Order.place(
            command.customerId(),
            command.idempotencyKey(),
            lines,
            clock.instant()
        );

        Money total = order.total();
        var authorization = paymentPort.authorize(command.customerId(), total, command.idempotencyKey());
        if (!authorization.accepted()) {
            throw ProblemDetailsException.paymentRejected(authorization.reason());
        }

        order.markPaymentAuthorized(authorization.authorizationId(), clock.instant());
        Order saved = orderRepository.save(order);
        outboxWriter.store(saved.pullDomainEvents());

        return Response
            .created(URI.create("/orders/" + saved.id().value()))
            .entity(OrderResponse.from(saved))
            .build();
    }
}

Plain Java

Datei: src/main/java/com/seb4u/demo/application/PlaceOrderService.java

package com.seb4u.demo.application;

import com.seb4u.demo.application.ports.CustomerAccessPolicy;
import com.seb4u.demo.application.ports.OrderRepository;
import com.seb4u.demo.application.ports.OutboxWriter;
import com.seb4u.demo.application.ports.PaymentPort;
import com.seb4u.demo.application.ports.ProductCatalogPort;
import com.seb4u.demo.application.ports.UnitOfWork;
import com.seb4u.demo.domain.Order;
import com.seb4u.demo.domain.OrderLine;
import com.seb4u.demo.shared.BusinessException;
import java.time.Clock;
import java.util.List;
import java.util.Objects;

public final class PlaceOrderService implements PlaceOrderUseCase {
    private final CustomerAccessPolicy accessPolicy;
    private final ProductCatalogPort productCatalog;
    private final PaymentPort paymentPort;
    private final OrderRepository orderRepository;
    private final OutboxWriter outboxWriter;
    private final UnitOfWork unitOfWork;
    private final Clock clock;

    public PlaceOrderService(
        CustomerAccessPolicy accessPolicy,
        ProductCatalogPort productCatalog,
        PaymentPort paymentPort,
        OrderRepository orderRepository,
        OutboxWriter outboxWriter,
        UnitOfWork unitOfWork,
        Clock clock
    ) {
        this.accessPolicy = Objects.requireNonNull(accessPolicy);
        this.productCatalog = Objects.requireNonNull(productCatalog);
        this.paymentPort = Objects.requireNonNull(paymentPort);
        this.orderRepository = Objects.requireNonNull(orderRepository);
        this.outboxWriter = Objects.requireNonNull(outboxWriter);
        this.unitOfWork = Objects.requireNonNull(unitOfWork);
        this.clock = Objects.requireNonNull(clock);
    }

    @Override
    public PlaceOrderResult place(PlaceOrderCommand command) {
        Objects.requireNonNull(command, "command");

        return unitOfWork.execute(() -> {
            accessPolicy.assertCanPlaceOrder(command.actorId(), command.customerId());

            var existing = orderRepository.findByIdempotencyKey(command.idempotencyKey());
            if (existing.isPresent()) {
                return PlaceOrderResult.fromExisting(existing.get());
            }

            List<OrderLine> lines = command.lines().stream()
                .map(line -> productCatalog.requireSellableProduct(
                    line.productId(),
                    line.quantity(),
                    command.currency()
                ))
                .toList();

            Order order = Order.place(
                command.customerId(),
                command.idempotencyKey(),
                lines,
                clock.instant()
            );

            var authorization = paymentPort.authorize(
                command.customerId(),
                order.total(),
                command.idempotencyKey().value()
            );

            if (!authorization.accepted()) {
                throw BusinessException.paymentRejected(authorization.reason());
            }

            order.markPaymentAuthorized(authorization.authorizationId(), clock.instant());
            Order saved = orderRepository.save(order);
            outboxWriter.store(saved.pullDomainEvents());

            return PlaceOrderResult.fromCreated(saved);
        });
    }
}

Vergleichsregel: Wenn die Fachlogik in allen drei Tabs fast gleich aussieht, ist die Architektur gesund. Wenn die Framework-Variante die Geschäftsregeln dominiert, ist die Grenze zwischen Application Core und Infrastruktur zu schwach.

API-Grenze: DTOs, Bean Validation und Mapping

DTOs sind bewusst nicht deine Domain. Sie sind Verträge mit der Außenwelt. Sie dürfen Validierungsannotationen, JSON-Namen und API-spezifische Defaults enthalten.

package com.seb4u.demo.spring.api;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import java.util.List;

public record PlaceOrderRequest(
    @NotBlank @Size(max = 80) String customerId,
    @NotBlank @Size(max = 80) String idempotencyKey,
    @NotBlank @Size(max = 3) String currency,
    @Valid @NotEmpty @Size(max = 100) List<LineRequest> lines,
    @Size(max = 40) String couponCode
) {
    public record LineRequest(
        @NotBlank @Size(max = 80) String productId,
        @Positive int quantity,
        @NotNull MoneyRequest unitPrice
    ) {}

    public record MoneyRequest(
        @NotBlank String amount,
        @NotBlank @Size(max = 3) String currency
    ) {}
}
package com.seb4u.demo.spring.api;

import com.seb4u.demo.spring.application.IdempotencyKey;
import com.seb4u.demo.spring.application.PlaceOrderCommand;
import com.seb4u.demo.spring.domain.CustomerId;
import com.seb4u.demo.spring.domain.Money;
import com.seb4u.demo.spring.domain.OrderLineDraft;
import com.seb4u.demo.spring.domain.ProductId;
import java.util.Currency;

final class PlaceOrderApiMapper {
    private PlaceOrderApiMapper() {}

    static PlaceOrderCommand toCommand(PlaceOrderRequest request, String authenticatedUserId) {
        var currency = Currency.getInstance(request.currency());

        var lines = request.lines().stream()
            .map(line -> new OrderLineDraft(
                new ProductId(line.productId()),
                line.quantity(),
                new Money(line.unitPrice().amount(), Currency.getInstance(line.unitPrice().currency()))
            ))
            .toList();

        return new PlaceOrderCommand(
            new CustomerId(request.customerId()),
            authenticatedUserId,
            new IdempotencyKey(request.idempotencyKey()),
            currency,
            lines,
            request.couponCode()
        );
    }
}

Controller: dünn, validierend, ohne Fachlogik

Der Controller validiert die API-Grenze, übersetzt DTOs und delegiert an den Use Case. Keine Preisberechnung, keine SQL-Statements, kein Payment-Code.

package com.seb4u.demo.spring.api;

import com.seb4u.demo.spring.application.PlaceOrderUseCase;
import jakarta.validation.Valid;
import java.net.URI;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/orders")
public final class OrderController {
    private final PlaceOrderUseCase placeOrder;

    public OrderController(PlaceOrderUseCase placeOrder) {
        this.placeOrder = placeOrder;
    }

    @PostMapping
    public ResponseEntity<OrderResponse> place(
        @Valid @RequestBody PlaceOrderRequest request,
        @AuthenticationPrincipal(expression = "name") String userId
    ) {
        var command = PlaceOrderApiMapper.toCommand(request, userId);
        var result = placeOrder.place(command);
        var response = OrderResponse.from(result);

        return ResponseEntity
            .created(URI.create("/api/orders/" + response.orderId()))
            .body(response);
    }
}

Application Port und Command

Der Command ist die stabile Eingabe des Use Cases. Er enthält bereits fachliche Typen und keine HTTP-Details mehr.

// file: PlaceOrderCommand.java
package com.seb4u.demo.spring.application;

import com.seb4u.demo.spring.domain.CustomerId;
import com.seb4u.demo.spring.domain.OrderLineDraft;
import java.util.Currency;
import java.util.List;

public record PlaceOrderCommand(
    CustomerId customerId,
    String authenticatedUserId,
    IdempotencyKey idempotencyKey,
    Currency currency,
    List<OrderLineDraft> lines,
    String couponCode
) {
    public PlaceOrderCommand {
        if (lines == null || lines.isEmpty()) {
            throw new IllegalArgumentException("Order must contain at least one line");
        }
        lines = List.copyOf(lines);
    }
}
// file: IdempotencyKey.java
package com.seb4u.demo.spring.application;

public record IdempotencyKey(String value) {
    public IdempotencyKey {
        if (value == null || value.isBlank() || value.length() > 80) {
            throw new IllegalArgumentException("Invalid idempotency key");
        }
    }
}
package com.seb4u.demo.spring.application;

public interface PlaceOrderUseCase {
    PlaceOrderResult place(PlaceOrderCommand command);
}

Enterprise Application Service: Transaktion, Idempotenz, Ports, Outbox

Dieses Beispiel ist bewusst komplexer als ein Mini-Tutorial. Es zeigt typische Enterprise-Fragen: Darf der User bestellen? Wurde der Request schon verarbeitet? Sind Produktpreise aktuell? Wo beginnt und endet die Transaktion? Wie werden Events zuverlässig veröffentlicht?

package com.seb4u.demo.spring.application;

import com.seb4u.demo.spring.application.ports.CustomerAccessPolicy;
import com.seb4u.demo.spring.application.ports.OrderRepository;
import com.seb4u.demo.spring.application.ports.OutboxWriter;
import com.seb4u.demo.spring.application.ports.PaymentPort;
import com.seb4u.demo.spring.application.ports.ProductCatalogPort;
import com.seb4u.demo.spring.domain.Order;
import com.seb4u.demo.spring.domain.OrderId;
import com.seb4u.demo.spring.domain.OrderPricing;
import com.seb4u.demo.spring.shared.BusinessException;
import com.seb4u.demo.spring.shared.ErrorCode;
import java.time.Clock;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public final class PlaceOrderService implements PlaceOrderUseCase {
    private final CustomerAccessPolicy accessPolicy;
    private final ProductCatalogPort productCatalog;
    private final PaymentPort payment;
    private final OrderRepository orders;
    private final OutboxWriter outbox;
    private final Clock clock;

    public PlaceOrderService(
        CustomerAccessPolicy accessPolicy,
        ProductCatalogPort productCatalog,
        PaymentPort payment,
        OrderRepository orders,
        OutboxWriter outbox,
        Clock clock
    ) {
        this.accessPolicy = accessPolicy;
        this.productCatalog = productCatalog;
        this.payment = payment;
        this.orders = orders;
        this.outbox = outbox;
        this.clock = clock;
    }

    @Override
    @Transactional
    public PlaceOrderResult place(PlaceOrderCommand command) {
        accessPolicy.assertCanPlaceOrder(command.authenticatedUserId(), command.customerId());

        var existing = orders.findByIdempotencyKey(command.customerId(), command.idempotencyKey());
        if (existing.isPresent()) {
            return PlaceOrderResult.alreadyAccepted(existing.get().id(), existing.get().status());
        }

        var catalogSnapshot = productCatalog.snapshotFor(command.lines());
        var pricing = OrderPricing.from(command.lines(), catalogSnapshot, command.currency(), command.couponCode());

        var order = Order.place(
            OrderId.newId(),
            command.customerId(),
            command.idempotencyKey().value(),
            pricing,
            clock.instant()
        );

        var authorization = payment.authorize(order.paymentRequest());
        order.markPaymentAuthorized(authorization.authorizationId(), clock.instant());

        orders.save(order);
        outbox.append(order.pullDomainEvents());

        return PlaceOrderResult.accepted(order.id(), order.status(), order.total());
    }
}

Domain Aggregate: Regeln im Modell, nicht im Controller

Die Domain schützt Invarianten: keine leere Bestellung, keine Statussprünge, keine Zahlung nach Storno, keine negativen Beträge.

package com.seb4u.demo.spring.domain;

import com.seb4u.demo.spring.shared.BusinessException;
import com.seb4u.demo.spring.shared.ErrorCode;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public final class Order {
    private final OrderId id;
    private final CustomerId customerId;
    private final String idempotencyKey;
    private final List<OrderLine> lines;
    private final List<DomainEvent> events = new ArrayList<>();
    private OrderStatus status;
    private Money total;
    private String paymentAuthorizationId;

    private Order(OrderId id, CustomerId customerId, String idempotencyKey, List<OrderLine> lines, Money total) {
        this.id = Objects.requireNonNull(id);
        this.customerId = Objects.requireNonNull(customerId);
        this.idempotencyKey = requireText(idempotencyKey, "idempotencyKey");
        this.lines = List.copyOf(lines);
        this.total = Objects.requireNonNull(total);
        this.status = OrderStatus.PENDING_PAYMENT;

        if (this.lines.isEmpty()) {
            throw new BusinessException(ErrorCode.EMPTY_ORDER, "Order must contain at least one line");
        }
    }

    public static Order place(OrderId id, CustomerId customerId, String idempotencyKey, OrderPricing pricing, Instant occurredAt) {
        var order = new Order(id, customerId, idempotencyKey, pricing.lines(), pricing.total());
        order.events.add(new OrderPlaced(id.value(), customerId.value(), pricing.total().amount(), occurredAt));
        return order;
    }

    public void markPaymentAuthorized(String authorizationId, Instant occurredAt) {
        if (status != OrderStatus.PENDING_PAYMENT) {
            throw new BusinessException(ErrorCode.INVALID_ORDER_STATE, "Only pending orders can be paid");
        }
        this.paymentAuthorizationId = requireText(authorizationId, "authorizationId");
        this.status = OrderStatus.PAID;
        this.events.add(new OrderPaymentAuthorized(id.value(), authorizationId, total.amount(), occurredAt));
    }

    public void cancel(String reason, Instant occurredAt) {
        if (!status.canBeCancelled()) {
            throw new BusinessException(ErrorCode.INVALID_ORDER_STATE, "Order cannot be cancelled from " + status);
        }
        this.status = OrderStatus.CANCELLED;
        this.events.add(new OrderCancelled(id.value(), reason, occurredAt));
    }

    public PaymentRequest paymentRequest() {
        if (status != OrderStatus.PENDING_PAYMENT) {
            throw new BusinessException(ErrorCode.INVALID_ORDER_STATE, "Payment request already consumed");
        }
        return new PaymentRequest(id.value(), customerId.value(), total.amount(), total.currency());
    }

    public List<DomainEvent> pullDomainEvents() {
        var copy = List.copyOf(events);
        events.clear();
        return copy;
    }

    public OrderId id() { return id; }
    public CustomerId customerId() { return customerId; }
    public OrderStatus status() { return status; }
    public Money total() { return total; }

    private static String requireText(String value, String field) {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException(field + " must not be blank");
        }
        return value;
    }
}
package com.seb4u.demo.spring.domain;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Currency;
import java.util.Objects;

public record Money(BigDecimal amount, Currency currency) implements Comparable<Money> {
    public Money(String amount, Currency currency) {
        this(new BigDecimal(amount), currency);
    }

    public Money {
        Objects.requireNonNull(amount, "amount");
        Objects.requireNonNull(currency, "currency");
        amount = amount.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_EVEN);
        if (amount.signum() < 0) {
            throw new IllegalArgumentException("Money must not be negative");
        }
    }

    public Money add(Money other) {
        assertSameCurrency(other);
        return new Money(amount.add(other.amount), currency);
    }

    public Money multiply(int factor) {
        if (factor < 0) {
            throw new IllegalArgumentException("factor must not be negative");
        }
        return new Money(amount.multiply(BigDecimal.valueOf(factor)), currency);
    }

    public Money percentage(String percent) {
        var ratio = new BigDecimal(percent).movePointLeft(2);
        return new Money(amount.multiply(ratio), currency);
    }

    private void assertSameCurrency(Money other) {
        if (!currency.equals(other.currency)) {
            throw new IllegalArgumentException("Currency mismatch: " + currency + " != " + other.currency);
        }
    }

    @Override
    public int compareTo(Money other) {
        assertSameCurrency(other);
        return amount.compareTo(other.amount);
    }
}

Ports: austauschbare Infrastruktur

Ports sind Interfaces im Application Core. Infrastruktur implementiert sie. Dadurch kannst du den Kern ohne Datenbank, Broker und Payment Provider testen.

package com.seb4u.demo.spring.application.ports;

import com.seb4u.demo.spring.application.IdempotencyKey;
import com.seb4u.demo.spring.domain.CustomerId;
import com.seb4u.demo.spring.domain.Order;
import com.seb4u.demo.spring.domain.OrderId;
import java.util.Optional;

public interface OrderRepository {
    Optional<Order> findById(OrderId id);
    Optional<Order> findByIdempotencyKey(CustomerId customerId, IdempotencyKey key);
    void save(Order order);
}
package com.seb4u.demo.spring.application.ports;

import com.seb4u.demo.spring.domain.DomainEvent;
import java.util.List;

public interface OutboxWriter {
    void append(List<DomainEvent> events);
}
package com.seb4u.demo.spring.application.ports;

import com.seb4u.demo.spring.domain.PaymentAuthorization;
import com.seb4u.demo.spring.domain.PaymentRequest;

public interface PaymentPort {
    PaymentAuthorization authorize(PaymentRequest request);
}

Infrastructure Adapter: JPA bleibt außerhalb der Domain

Die JPA-Entity ist ein Persistenzmodell, nicht dein Domain-Modell. In großen Systemen kann ein Mapper zwischen Domain und Entity sinnvoll sein, auch wenn das mehr Code bedeutet.

package com.seb4u.demo.spring.infrastructure.persistence;

import com.seb4u.demo.spring.application.IdempotencyKey;
import com.seb4u.demo.spring.application.ports.OrderRepository;
import com.seb4u.demo.spring.domain.CustomerId;
import com.seb4u.demo.spring.domain.Order;
import com.seb4u.demo.spring.domain.OrderId;
import java.util.Optional;
import org.springframework.stereotype.Repository;

@Repository
public final class JpaOrderRepository implements OrderRepository {
    private final SpringDataOrderJpaRepository jpa;
    private final OrderPersistenceMapper mapper;

    public JpaOrderRepository(SpringDataOrderJpaRepository jpa, OrderPersistenceMapper mapper) {
        this.jpa = jpa;
        this.mapper = mapper;
    }

    @Override
    public Optional<Order> findById(OrderId id) {
        return jpa.findById(id.value()).map(mapper::toDomain);
    }

    @Override
    public Optional<Order> findByIdempotencyKey(CustomerId customerId, IdempotencyKey key) {
        return jpa.findByCustomerIdAndIdempotencyKey(customerId.value(), key.value()).map(mapper::toDomain);
    }

    @Override
    public void save(Order order) {
        jpa.save(mapper.toEntity(order));
    }
}

Outbox Writer: Events atomar mit der Bestellung speichern

Events werden nicht direkt im Use Case an Kafka/RabbitMQ geschickt. Erst werden Bestellung und Outbox-Event in derselben Transaktion gespeichert; ein Worker publiziert später.

package com.seb4u.demo.spring.infrastructure.outbox;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.seb4u.demo.spring.application.ports.OutboxWriter;
import com.seb4u.demo.spring.domain.DomainEvent;
import java.time.Clock;
import java.util.List;
import java.util.UUID;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.stereotype.Component;

@Component
public final class JdbcOutboxWriter implements OutboxWriter {
    private final JdbcClient jdbc;
    private final ObjectMapper objectMapper;
    private final Clock clock;

    public JdbcOutboxWriter(JdbcClient jdbc, ObjectMapper objectMapper, Clock clock) {
        this.jdbc = jdbc;
        this.objectMapper = objectMapper;
        this.clock = clock;
    }

    @Override
    public void append(List<DomainEvent> events) {
        for (var event : events) {
            jdbc.sql("""
                insert into outbox_event(id, aggregate_id, event_type, payload, occurred_at, published_at)
                values (:id, :aggregateId, :eventType, cast(:payload as jsonb), :occurredAt, null)
                """)
                .param("id", UUID.randomUUID())
                .param("aggregateId", event.aggregateId())
                .param("eventType", event.eventType())
                .param("payload", toJson(event))
                .param("occurredAt", event.occurredAt(clock))
                .update();
        }
    }

    private String toJson(DomainEvent event) {
        try {
            return objectMapper.writeValueAsString(event);
        } catch (Exception ex) {
            throw new IllegalStateException("Could not serialize domain event " + event.eventType(), ex);
        }
    }
}

Fehlerbehandlung: Fachliche Fehler als API Problem Details

Enterprise-Code braucht vorhersehbare Fehlerverträge. Ein RuntimeException("bad request") ist dafür zu unpräzise.

// file: ErrorCode.java
package com.seb4u.demo.spring.shared;

public enum ErrorCode {
    EMPTY_ORDER,
    INVALID_ORDER_STATE,
    PAYMENT_DECLINED,
    PRODUCT_NOT_FOUND,
    ACCESS_DENIED,
    IDEMPOTENCY_CONFLICT
}
// file: BusinessException.java
package com.seb4u.demo.spring.shared;

public final class BusinessException extends RuntimeException {
    private final ErrorCode code;

    public BusinessException(ErrorCode code, String message) {
        super(message);
        this.code = code;
    }

    public ErrorCode code() {
        return code;
    }
}
package com.seb4u.demo.spring.api;

import com.seb4u.demo.spring.shared.BusinessException;
import java.net.URI;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public final class ApiExceptionHandler {
    @ExceptionHandler(BusinessException.class)
    ProblemDetail handleBusiness(BusinessException ex) {
        var status = switch (ex.code()) {
            case ACCESS_DENIED -> HttpStatus.FORBIDDEN;
            case PRODUCT_NOT_FOUND -> HttpStatus.NOT_FOUND;
            case PAYMENT_DECLINED, IDEMPOTENCY_CONFLICT, INVALID_ORDER_STATE, EMPTY_ORDER -> HttpStatus.CONFLICT;
        };
        var problem = ProblemDetail.forStatusAndDetail(status, ex.getMessage());
        problem.setType(URI.create("https://orderflow.dev/problems/" + ex.code().name().toLowerCase()));
        problem.setTitle(ex.code().name());
        problem.setProperty("code", ex.code().name());
        return problem;
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
        var problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Request validation failed");
        var fields = ex.getBindingResult().getFieldErrors().stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .toList();
        problem.setProperty("fields", fields);
        return problem;
    }
}

Enterprise Tests: Domain ohne Spring, Use Case mit Fakes

Teste die Domain schnell und isoliert. Teste den Application Service mit Ports als Fakes. Die Datenbank kommt erst in Adapter-Tests dazu.

package com.seb4u.demo.spring.domain;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.time.Instant;
import org.junit.jupiter.api.Test;

class OrderTest {
    @Test
    void paidOrderCannotBePaidTwice() {
        var order = TestOrders.pendingOrder();

        order.markPaymentAuthorized("auth-1", Instant.parse("2026-01-01T10:00:00Z"));

        assertThatThrownBy(() -> order.markPaymentAuthorized("auth-2", Instant.parse("2026-01-01T10:01:00Z")))
            .isInstanceOf(BusinessException.class)
            .hasMessageContaining("Only pending orders can be paid");
    }

    @Test
    void placingOrderRaisesOrderPlacedEvent() {
        var order = TestOrders.pendingOrder();

        var events = order.pullDomainEvents();

        assertThat(events).hasExactlyElementsOfTypes(OrderPlaced.class);
        assertThat(order.pullDomainEvents()).isEmpty();
    }
}
package com.seb4u.demo.spring.application;

import static org.assertj.core.api.Assertions.assertThat;

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import org.junit.jupiter.api.Test;

class PlaceOrderServiceTest {
    @Test
    void secondRequestWithSameIdempotencyKeyReturnsExistingOrder() {
        var orders = new InMemoryOrderRepository();
        var outbox = new RecordingOutboxWriter();
        var service = new PlaceOrderService(
            new AllowAllCustomerAccessPolicy(),
            new StubProductCatalogPort(),
            new AlwaysAuthorizedPaymentPort(),
            orders,
            outbox,
            Clock.fixed(Instant.parse("2026-01-01T10:00:00Z"), ZoneOffset.UTC)
        );
        var command = TestCommands.placeOrder("idem-123");

        var first = service.place(command);
        var second = service.place(command);

        assertThat(second.orderId()).isEqualTo(first.orderId());
        assertThat(second.acceptance()).isEqualTo(OrderAcceptance.ALREADY_ACCEPTED);
        assertThat(outbox.recordedEvents()).hasSize(2); // placed + payment-authorized only once
    }
}

Warum dieser Code enterprise-ready ist

Fähigkeit Umsetzung im Code
Lesbarkeit klare Namen, fachliche Typen, kleine Rollen
Änderbarkeit Ports isolieren Payment, Produktkatalog, Persistenz und Outbox
Testbarkeit Domain ohne Spring; Application Service mit Fakes
Konsistenz @Transactional um Bestellung + Outbox
Resilienz idempotenter Use Case gegen doppelte Requests
Security-Grenze Access Policy im Application Layer
API-Stabilität DTOs und Problem Details an der HTTP-Grenze
Fachlichkeit Invarianten im Aggregate statt in Controllern

Enterprise-Code-Review-Checkliste


Phase 7: Antipatterns und Code Smells

Ziel

Du erkennst schlechte Strukturen früh und kannst sie in konkrete Refactoring-Schritte übersetzen.

Antipattern-Katalog

Antipattern Symptome Ursache Gegenmaßnahme
God Service eine Klasse kann alles fehlende Verantwortungsgrenzen Services schneiden, Domain stärken
Big Ball of Mud alles hängt mit allem zusammen keine Architekturregeln Module, Ports, Tests, Abhängigkeitsregeln
Anemic Domain Model Domain hat nur Getter/Setter Fachlogik in Services Verhalten in Domain verschieben
Golden Hammer überall dasselbe Pattern Tool-/Pattern-Fixierung Problem zuerst beschreiben
Lava Flow alter Code bleibt ungenutzt liegen Angst vor Entfernen Tests + Dead-Code-Analyse
Shotgun Surgery kleine Änderung betrifft viele Dateien falsche Verteilung von Verantwortung zusammengehöriges Verhalten bündeln
Copy-Paste Programming ähnliche Blöcke überall kurzfristige Geschwindigkeit Abstraktion nach dritter Wiederholung
Distributed Monolith Microservices müssen gemeinsam deployt werden falsche Servicegrenzen Bounded Contexts, Events, Verträge
Chatty Service viele synchrone Remote Calls falsche API-Schnitte gröbere APIs, Caching, asynchrone Events
Transaction Script Overload Use Cases sind riesige Skripte Domainregeln nicht modelliert Domain Services und Entities einführen

Bildlicher Input: Smell zu Gegenmaßnahme

flowchart LR
    S[Symptom] --> Q{Welche Art?}
    Q -->|zu groß| A[Extract Class / Extract Function]
    Q -->|zu viele ifs| B[Strategy / State]
    Q -->|falsche Abhängigkeit| C[Dependency Inversion / Port]
    Q -->|Datenklumpen| D[Introduce Parameter Object / Value Object]
    Q -->|Duplikat| E[Extract Function / Template Method]

Übung in OrderFlow

Baue absichtlich eine kleine schlechte Version eines LegacyOrderService und diagnostiziere sie:

  • Welche Smells siehst du?
  • Welche Risiken entstehen?
  • Welche Tests brauchst du vor dem Refactoring?
  • Welches Refactoring kommt zuerst?

Phase 8: Refactoring an Legacy-Code

Ziel

Du verbesserst bestehenden Code schrittweise, ohne sein beobachtbares Verhalten unkontrolliert zu ändern.

Refactoring-Loop

flowchart LR
    A[Code Smell erkennen] --> B[Test oder Characterization Test schreiben]
    B --> C[Kleine Änderung]
    C --> D[Tests ausführen]
    D --> E{Grün?}
    E -->|Ja| F[Commit]
    E -->|Nein| G[Rückgängig / korrigieren]
    G --> C
    F --> A

Refactoring-Techniken

Smell Refactoring Ziel
Lange Methode Extract Function Lesbarkeit
Große Klasse Extract Class Verantwortung trennen
Primitive Obsession Introduce Value Object Fachlichkeit ausdrücken
Lange Parameterliste Introduce Parameter Object Signatur stabilisieren
Verschachtelte Bedingungen Guard Clauses Kontrollfluss vereinfachen
Switch über Typen Replace Conditional with Polymorphism Verhalten kapseln
Duplikate Extract Function / Pull Up Wiederholung reduzieren
Feature Envy Move Function Verhalten an richtige Stelle

Characterization Test

@Test
void legacyServiceKeepsCurrentVipDiscountBehavior() {
    var service = new LegacyOrderService(fakeRepo, fakePayment);

    var result = service.placeOrder(vipOrderRequest());

    assertThat(result.total()).isEqualByComparingTo("90.00");
}

Dieser Test sagt nicht, dass das Verhalten fachlich perfekt ist. Er hält zunächst fest, was aktuell passiert.


Phase 9: Durchgehender Refactoring-Fall: vom God Service zur Architektur

Ausgangspunkt: schlechter OrderService

public class OrderService {
    public String place(OrderRequest request) {
        if (request == null || request.items() == null || request.items().isEmpty()) {
            throw new RuntimeException("bad request");
        }

        BigDecimal total = BigDecimal.ZERO;
        for (var item : request.items()) {
            if (item.qty() <= 0) throw new RuntimeException("bad qty");
            if (item.price().compareTo(BigDecimal.ZERO) <= 0) throw new RuntimeException("bad price");
            total = total.add(item.price().multiply(BigDecimal.valueOf(item.qty())));
        }

        if (request.coupon() != null && request.coupon().equals("VIP10")) {
            total = total.multiply(new BigDecimal("0.90"));
        }

        var paymentOk = paymentClient.pay(request.cardToken(), total);
        if (!paymentOk) throw new RuntimeException("payment failed");

        var id = UUID.randomUUID().toString();
        jdbc.update("insert into orders(id, customer_id, total, status) values (?, ?, ?, ?)",
            id, request.customerId(), total, "PAID");

        emailClient.send(request.email(), "Order placed: " + id);
        return id;
    }
}

Diagnose

Problem Risiko
Validierung, Preisberechnung, Zahlung, Persistenz und E-Mail in einer Methode Änderung ist riskant
RuntimeException überall Fehler lassen sich schlecht behandeln
direkte externe Clients schlecht testbar
SQL direkt im Use Case Infrastruktur leckt in Application Layer
Rabattcode hart codiert Erweiterung führt zu weiteren ifs
Geld als BigDecimal ohne Value Object Rundung/Währung unklar

Schritt 1: Tests sichern

@Test
void vipCouponReducesTotalByTenPercent() {
    var result = service.place(orderWithCoupon("VIP10"));
    assertThat(result.total()).isEqualByComparingTo("90.00");
}

Schritt 2: Value Objects einführen

public record Money(BigDecimal amount, Currency currency) {
    public Money {
        Objects.requireNonNull(amount);
        Objects.requireNonNull(currency);
        if (amount.scale() > 2) {
            throw new IllegalArgumentException("Too many decimals");
        }
    }

    public static Money eur(String value) {
        return new Money(new BigDecimal(value), Currency.getInstance("EUR"));
    }
}

Schritt 3: Rabatt als Strategy

public final class CouponDiscountPolicy implements DiscountPolicy {
    public Money apply(OrderDraft draft) {
        if (draft.coupon().isPresent() && draft.coupon().get().equals("VIP10")) {
            return draft.subtotal().percentage("10");
        }
        return Money.zero(draft.currency());
    }
}

Schritt 4: Payment als Port

public interface PaymentPort {
    PaymentAuthorization authorize(PaymentCommand command);
}

Schritt 5: Repository statt SQL im Use Case

public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(OrderId id);
}

Schritt 6: Application Service nach Refactoring

public final class PlaceOrderService {
    private final PricingService pricing;
    private final PaymentPort payment;
    private final OrderRepository orders;
    private final DomainEventPublisher events;

    public OrderId place(PlaceOrderCommand command) {
        var draft = OrderDraft.from(command);
        var priced = pricing.price(draft);
        var authorization = payment.authorize(PaymentCommand.from(priced));
        var order = Order.place(priced, authorization);
        orders.save(order);
        events.publish(order.pullEvents());
        return order.id();
    }
}

Ergebnisvergleich

Vorher Nachher
eine lange Methode mehrere fokussierte Objekte
schwer testbar Ports und Domain separat testbar
Rabatt per if Strategy
SQL im Service Repository
externe API direkt Adapter
unklare Fehler fachliche Exceptions

Phase 10: Entscheidungs- und Diagnosekarten

Pattern-Entscheidungsbaum

flowchart TD
    A[Welches Designproblem hast du?]
    A --> B{Objekterzeugung ist komplex?}
    B -->|viele optionale Felder| Builder
    B -->|Provider abhängig von Typ| Factory
    B -->|Familien verwandter Objekte| AbstractFactory[Abstract Factory]
    A --> C{Verhalten soll austauschbar sein?}
    C -->|Rabatt / Versand / Sortierung| Strategy
    C -->|Status verändert erlaubte Aktionen| State
    C -->|Anfrage als Objekt behandeln| Command
    A --> D{Fremde Schnittstelle passt nicht?}
    D --> Adapter
    A --> E{Viele Empfänger sollen reagieren?}
    E --> Observer
    A --> F{Zusatzverhalten ohne Vererbung?}
    F --> Decorator
    A --> G{Komplexes Subsystem vereinfachen?}
    G --> Facade

Entscheidungsfragen

  1. Wird das Problem durch Polymorphie einfacher?
  2. Ist die zusätzliche Indirektion den Nutzen wert?
  3. Kann ein Test zeigen, dass das Pattern die Änderung erleichtert?
  4. Gibt es eine einfachere Lösung mit Methode, Klasse oder Record?
  5. Wäre das Pattern auch für eine zweite Variante nützlich?

Antipattern-Erkennungskarte

flowchart TB
    A[Symptom im Code] --> B{Änderung riskant?}
    B -->|Ja, viele Dateien| Shotgun[Shotgun Surgery]
    B -->|Ja, eine riesige Datei| God[God Service]
    A --> C{Domain ohne Verhalten?}
    C -->|Ja| Anemic[Anemic Domain Model]
    A --> D{Alles synchron gekoppelt?}
    D -->|Ja| Distributed[Distributed Monolith]
    A --> E{Alte unklare Codepfade?}
    E -->|Ja| Lava[Lava Flow]
    A --> F{Gleiches Pattern überall?}
    F -->|Ja| Hammer[Golden Hammer]

Diagnoseformular

Name des Smells:
Fundstelle:
Symptome:
Risiko:
Vermutete Ursache:
Erster sicherer Refactoring-Schritt:
Benötigter Test:
Definition of Done:

Enterprise-Architekturkarten

Karte 1: API-Grenze

HTTP Request
  ↓ validieren
DTO / Command
  ↓ mappen
Application Service
  ↓ nutzt Domain
Domain Model

Regel: Request-DTOs sind keine Domain-Objekte.

Karte 2: Transaktionsgrenze

Use Case Start
  ↓
Daten laden
  ↓
Domain-Regel ausführen
  ↓
Daten speichern + Outbox schreiben
  ↓
Commit
  ↓
Externe Effekte asynchron verarbeiten

Regel: Externe Netzwerkeffekte möglichst nicht mitten in der kritischen Datenbanktransaktion verstecken.

Karte 3: Ports & Adapter

Application Core -> PaymentPort -> StripePaymentAdapter -> Stripe API
Application Core -> OrderRepository -> JpaOrderRepository -> Database

Regel: Der Kern kennt Interfaces, nicht technische Details.


Phase 11: Übungen, Katas und Portfolio-Aufgaben

Wöchentliche Katas

Woche Kata Aufgabe
1 Naming Kata 20 schlechte Namen verbessern
2 Function Split eine 80-Zeilen-Methode zerlegen
3 Encapsulation Kata Setter entfernen, Invarianten schützen
4 Value Object Kata Money, Quantity, Email bauen
5 Builder Kata lesbare Testdaten erzeugen
6 Exception Kata technische Fehler in fachliche Fehler übersetzen
7 Strategy Kata Rabattlogik austauschbar machen
8 Adapter Kata externe Payment-API verstecken
9 Observer Kata Domain Events sammeln und veröffentlichen
10 Repository Kata Datenzugriff kapseln
11 DTO Mapping Kata API und Domain trennen
12 Outbox Kata Event zuverlässig speichern
13 Smell Hunt 15 Smells im Legacy-Code markieren
14 Refactoring Kata God Service schrittweise schneiden
15 Review Kata Pull Request mit Checkliste prüfen
16 Portfolio Architektur und Refactoring erklären

Portfolio-Artefakte

Am Ende solltest du diese Nachweise haben:

  • Architekturdiagramm von OrderFlow.
  • ADRs für mindestens 5 Entscheidungen.
  • Vorher/Nachher-Refactoring-Dokumentation.
  • Teststrategie mit Beispielen.
  • Pattern-Map: Welche Patterns wurden eingesetzt und warum?
  • Antipattern-Katalog mit Fundstellen und Korrekturen.
  • Kurzes Demo-Skript für ein Interview oder Review.

Phase 12: Code-Review-Checklisten

Clean Code Review

Effective Java Review

Pattern Review

Enterprise Review

Refactoring Review


Phase 13: Abschlussprojekt

Aufgabe

Baue OrderFlow so weit aus, dass folgende Use Cases funktionieren:

  1. Bestellung platzieren.
  2. Rabattregel anwenden.
  3. Zahlung autorisieren.
  4. Bestellung speichern.
  5. OrderPlaced Event in Outbox schreiben.
  6. Rechnungserstellung simulieren.
  7. Bestellung stornieren, solange sie nicht versendet wurde.
  8. Fehlerfälle sauber in API-Antworten übersetzen.

Muss-Kriterien

  • Mindestens 5 Value Objects.
  • Mindestens 3 Patterns bewusst eingesetzt.
  • Mindestens 3 Enterprise Patterns eingesetzt.
  • Mindestens 1 Antipattern dokumentiert und behoben.
  • Mindestens 1 großer Refactoring-Fall mit Vorher/Nachher-Vergleich.
  • Unit Tests für Domain.
  • Application Tests für Use Cases.
  • Mindestens eine Architekturregel oder Modulregel.

Bewertungsrubrik

Bereich Anfänger Solide Stark
Lesbarkeit Code läuft, aber Namen unklar Namen und Methoden meist klar Code liest sich wie Fachsprache
Domain Design Datenklassen mit Services einige Regeln in Domain Invarianten konsequent geschützt
Patterns zufällig oder übertrieben passende Patterns für Varianten Patterns reduzieren echte Änderungsrisiken
Enterprise Controller/Service/Repo grob getrennt klare Layer Ports, Transaktionen, DTOs und Events sauber
Refactoring große riskante Änderungen kleine Schritte mit Tests Refactoring-Journal und saubere Commit-Historie
Tests wenige Happy Paths wichtige Regeln getestet Tests treiben Design und sichern Refactoring

Phase 14: Struktur-Review und Lernpfad-Nutzung

Neue Struktur auf einen Blick

Diese Version trennt bewusst zwischen Lernen, Anwenden und Nachweisen:

Bereich Kapitel Zweck
Lernen Phase 0–5 Begriffe, Prinzipien und Entscheidungsmodelle aufbauen
Anwenden Phase 6–9 Enterprise-Code schreiben und Legacy-Code verbessern
Nachweisen Phase 10–13 Diagnosekarten, Übungen, Reviews und Abschlussprojekt dokumentieren

Empfohlene Nutzung: Lies nicht alles linear. Arbeite pro Woche ein vertikales Stück: ein Konzept, ein Codebeispiel, eine Übung, ein Review, ein Commit.


Quellen und weiterführende Literatur

Offizielle und primäre Quellen

Bücher

  • Robert C. Martin: Clean Code.
  • Joshua Bloch: Effective Java.
  • Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Design Patterns.
  • Martin Fowler: Refactoring.
  • Martin Fowler: Patterns of Enterprise Application Architecture.
  • Eric Evans: Domain-Driven Design.
  • Michael Feathers: Working Effectively with Legacy Code.

Persönliche Lernnotiz

Dieser Lernpfad ist nicht zum einmaligen Lesen gedacht. Er wird stark, wenn du ihn mit deinem eigenen Code kombinierst: jede Woche ein kleiner Umbau, ein Review, ein Test und ein dokumentierter Architekturentscheid.

⌂ Cockpit