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:
- 60–90 Minuten lesen und Beispiele nachvollziehen.
- 2–4 Stunden im Projekt
OrderFlowumsetzen. - 60 Minuten Tests schreiben oder verbessern.
- 45 Minuten Review mit Checkliste.
- 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,tsagen wenig aus.- Validierung, Berechnung und Persistenz sind vermischt.
doubleist 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
- Erstelle
PlaceOrderCommand. - Zerlege eine lange Methode in 4–6 sprechende Methoden oder Klassen.
- Ersetze primitive Geldbeträge durch
Money. - Schreibe Tests für Preisberechnung und leere Warenkörbe.
- 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
- Modelliert
Order,OrderLine,Money,Quantity,OrderStatus. - Verschiebt fachliche Regeln aus Services in Domain-Objekte.
- Macht Collections nach außen unveränderbar.
- Ersetzt Status-Strings durch
enumoder 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,Emailals 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
- Baue
DiscountPolicyals Strategy. - Kapsle Payment mit Adapter.
- Erzeuge Payment-Provider über Factory.
- Führe Domain Events für
OrderPlacedein. - 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 verwendencom.seb4u.demo.jakarta.... Framework-neutrale Java-Beispiele verwendencom.seb4u.demo.... Vermeide generische Demo-Packages; alle vollständigen Java-Dateien bekommen explizit einpackagenach 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
- Wird das Problem durch Polymorphie einfacher?
- Ist die zusätzliche Indirektion den Nutzen wert?
- Kann ein Test zeigen, dass das Pattern die Änderung erleichtert?
- Gibt es eine einfachere Lösung mit Methode, Klasse oder Record?
- 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:
- Bestellung platzieren.
- Rabattregel anwenden.
- Zahlung autorisieren.
- Bestellung speichern.
OrderPlacedEvent in Outbox schreiben.- Rechnungserstellung simulieren.
- Bestellung stornieren, solange sie nicht versendet wurde.
- 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
- Oracle Java SE 25 API,
Optional: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Optional.html - Oracle Java SE 25, Virtual Threads: https://docs.oracle.com/en/java/javase/25/core/virtual-threads.html
- Oracle Java SE 21 Language Guide, Records: https://docs.oracle.com/en/java/javase/21/language/records.html
- Oracle Java Tutorials, try-with-resources: https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html
- OpenJDK JEP 444, Virtual Threads: https://openjdk.org/jeps/444
- OpenJDK JDK 25 Project: https://openjdk.org/projects/jdk/25/
- Martin Fowler, Catalog of Refactorings: https://refactoring.com/catalog/
- Martin Fowler, Patterns of Enterprise Application Architecture Catalog: https://martinfowler.com/eaaCatalog/
- Martin Fowler, Service Layer: https://martinfowler.com/eaaCatalog/serviceLayer.html
- OWASP Top 10:2025: https://owasp.org/Top10/2025/
- OWASP A01:2025 Broken Access Control: https://owasp.org/Top10/2025/A01_2025-Broken_Access_Control/
- Spring Framework Reference, Transaction Management: https://docs.spring.io/spring-framework/reference/data-access/transaction.html
- Spring Framework Reference, Bean Validation: https://docs.spring.io/spring-framework/reference/core/validation/beanvalidation.html
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.