Neu erstellte Sidebar-Version

Enterprise Java 11, 17 und 21

Dieses eigenständige Grundlagenbuch verbindet Grundlagen und Nachschlagewissen für Java 11, 17 und 21 mit praxisnaher Enterprise-Entwicklung. Es behandelt LTS-Strategien und Migration, moderne Sprach- und JVM-Funktionen, Nebenläufigkeit und Virtual Threads, Architektur- und Integrationsmuster, JPA und Transaktionen, REST, Messaging und Outbox, Security, Testing, Observability, Maven sowie die Einordnung von Spring Boot, Jakarta EE, Quarkus und Micronaut. Referenzkapitel, Diagramme und echte Code-Labs machen die Ausgabe zugleich Lernbuch, Entscheidungsgrundlage und Arbeitsreferenz.

9Themen
58Inhalte
6tiefere Punkte
Java 21Code-Labs
OrientierungEinstieg, Nutzung und empfohlene Lernreihenfolge.
Buchüberblick

Enterprise Java 11, 17 und 21 - Alles-in-einem Lese- und Referenzbuch

Diese konsolidierte Fassung bündelt die wichtigsten Inhalte des Pakets in einer zentralen Lesedatei. Die Desktop-HTML nutzt das breite PC-Layout aus; die iPhone/Safari-HTML bleibt robust und ohne kritische JavaScript-Abhängigkeit nutzbar.

Lesestrategie

  • Für PC: ALLES_IN_EINEM_ENTERPRISE_JAVA_BUCH_PC.html öffnen.
  • Für iPhone/iPad/Safari: ALLES_IN_EINEM_ENTERPRISE_JAVA_BUCH_IPHONE_SAFARI.html öffnen.
  • Für Druck/Archiv: ALLES_IN_EINEM_ENTERPRISE_JAVA_BUCH_DRUCKFASSUNG.pdf nutzen.

Inhaltliche Landkarte

text
Java 11/17/21 -> Architektur -> Daten -> APIs -> Messaging -> Testing -> Performance -> Migration -> Framework-Labs -> Referenz -> Code
So funktioniert das Buch

So nutzt du dieses Lernpaket

Kompakte Abschluss-Landkarte für das Enterprise-Java-11/17/21-Lese- und Referenzbuch.

Was dich erwartet

Diese Abschlussversion ist als Lese- und Referenzbuch gedacht: Du kannst sie wie ein Buch lesen, aber auch wie eine Nachschlageplattform verwenden. Die vorherigen Ausbaustufen bleiben nicht als Archiv herumliegen, sondern sind inhaltlich als Lernschichten im Gesamtpaket integriert.

Die finale Politur konzentriert sich auf Orientierung, saubere Öffnungsreihenfolge, bessere Portal-Navigation, kompaktere PDF-Führung, zusätzliche Praxisübungen und nachvollziehbare Abschlusschecks.

Finale Leseroute

Empfohlene Öffnungsreihenfolge

Schritt Datei Zweck
1 lernweg.html Einstieg, kurze Reihenfolge, Hinweise für Desktop und iPhone/Safari
2 index.html zentrale Navigation zu Buch, Themen, Referenz, Code und Checks
3 buch/enterprise-java-finales-portal-handbuch.html diese kompakte Landkarte als Abschluss-Handbuch
4 buch/enterprise-java-masterbuch.html Grundlagen, Java 11/17/21 und Enterprise-Basis
5 themen/*.html gezielte Vertiefung nach Thema
6 referenz/*.html Nachschlagen: Patterns, Glossar, Matrizen, Annotationen, Maven
7 code/**/README.html praktische Labs und Beispielprojekte
8 checks/CHECK_FINAL.html Abschlussstatus und Grenzen der Prüfung

Für PDF-Lesen ist die beste Reihenfolge: Portal-Handbuch, dann Masterbuch, dann Version-2/3/4-PDFs nach Bedarf.

Was fachlich enthalten ist

Das Paket behandelt Java 11, Java 17 und Java 21 aus Enterprise-Sicht. Der Schwerpunkt liegt nicht auf isolierten Sprachfeatures, sondern auf der Frage: Wie baut, modernisiert, prüft und betreibt man Enterprise-Java-Systeme?

Wichtige fachliche Blöcke:

  • Java-LTS-Vergleich und Migration von älteren Codebasen.
  • Enterprise-Architektur mit Layering, Hexagonal Architecture und DDD-Aggregaten.
  • REST, DTOs, Fehlerbehandlung, Security und Observability.
  • JPA/Hibernate, Transaktionen, Locking und Datenzugriff.
  • Messaging, Outbox Pattern, Events und Integrationsflüsse.
  • Performance, JVM-Basics, Virtual Threads und Java-21-Nebenläufigkeit.
  • Framework-Orientierung: Spring Boot, Jakarta EE, Quarkus und Micronaut.

Finale Paketstruktur

Was technisch enthalten ist

Das Paket enthält mehrere Code-Labs. Die wichtigsten sind:

Lab Zweck Prüfstatus
code/enterprise-order-billing-demo erste Order/Billing-Domäne mit Ports, Adapter, Outbox javac --release 21 OK
code/enterprise-java-platform-lab größeres Java-21-Lab mit Hexagonal Architecture und Refactoring javac --release 21 OK
code/framework-labs/order-platform-core gemeinsamer Core für getrennte Framework-Adapter javac --release 21 OK
code/runtime-modernization-lab Runtime-/Dependency-Governance als JDK-only Lab javac --release 21 OK

Die Framework-Adapter enthalten bewusst Spring/Jakarta/Quarkus/Micronaut-Importe. Diese sind als Projektstruktur und Lerncode enthalten; ein echter Build benötigt Maven und Internetzugriff auf Repositories.

Entwurfsmuster im Paket

Entwurfsmuster sind im Code nicht versteckt, sondern bewusst markiert. Zusätzlich liegen in den jeweiligen docs/design-patterns.md und docs/design-patterns.html Dateien Beschreibungen mit Pattern, Zweck, Einsatzort und Begründung.

Besonders wichtig:

  • Repository Pattern für Datenzugriff hinter Ports.
  • Ports and Adapters / Hexagonal Architecture für Framework-Unabhängigkeit.
  • Strategy Pattern für austauschbare Preis-/Risiko-/Entscheidungslogik.
  • Factory Pattern für kontrollierte Erzeugung von Entscheidungen oder Domänenobjekten.
  • Facade Pattern für lesbare Query- oder API-Zugänge.
  • Outbox Pattern für robuste Event-Veröffentlichung nach Transaktionen.
  • Saga / Process Manager im Framework-Core für mehrstufige Fulfillment-Flows.

Praxisübungen für die nächste Lernrunde

Die Übungen sind bewusst nicht als neues Großprojekt angelegt, sondern als Aufgaben, die du mit dem vorhandenen Material lösen kannst.

text
Übung 1: Lies den Order-Fulfillment-Flow im Core und zeichne ihn als eigene Sequenz.
Übung 2: Finde im Code alle Ports und Adapter und ordne sie fachlich zu.
Übung 3: Erweitere Money um eine Rundungsregel und prüfe Auswirkungen.
Übung 4: Baue eine neue PricingStrategy ein und dokumentiere das Pattern.
Übung 5: Erweitere das Runtime-Lab um eine neue RuntimeLine.
Übung 6: Schreibe einen ADR-Entwurf für Java 17 vs Java 21.
Übung 7: Beschreibe, welche Dateien du für einen echten Spring-Boot-Start zuerst anfassen würdest.
Übung 8: Erstelle eine Migration-Checkliste von Java 8 auf Java 21.

Die ausführlichere Übungssammlung liegt in referenz/leseplan-und-praxisuebungen.html.

PDF- und HTML-Hinweise

Die HTML-Dateien sind offline-fähig, verwenden lokale CSS-/JS-Dateien und sind auch ohne JavaScript grundsätzlich lesbar. JavaScript wird nur für Komfortfunktionen wie Suche, Treffer-Navigation, Copy-Button und Auf-/Zuklappen verwendet.

Die PDFs sind als Lesefassungen gedacht. Das finale Portal-PDF hat bewusst ein kurzes sichtbares Inhaltsverzeichnis, damit der Einstieg nicht von zu vielen Unterpunkten überladen wird. Für vollständige Navigation ist HTML besser geeignet.

Abschlussstatus

Finale Qualitätsschleife

Die finale Prüfung umfasst:

  • lokale HTML-Linkprüfung ohne externe Links,
  • Vorhandensein der zentralen Startdateien,
  • Renderprobe des finalen Portal-PDFs,
  • javac --release 21 für JDK-only Labs,
  • Demo-Runs der prüfbaren Labs,
  • Manifest, SHA-256-Checksummen und ZIP-Test.

Details stehen in checks/CHECK_FINAL.html und checks/CHECK_ZIP.html.

Leseplan & Praxisübungen

Leseplan und Praxisübungen

Übungen, Lernrouten und kleine Arbeitsaufträge für das Enterprise-Java-Paket.

Lernrouten

Route Für wen Reihenfolge
Schnellstart Überblick gewinnen START_HERE -> Portal-Handbuch -> Masterbuch-Kapitel 1-3
Entwickler Code verstehen Masterbuch -> Codebeispiele -> Framework-Core -> Checks
Architekt Systemgrenzen verstehen Architektur -> DDD -> Outbox -> Framework-Matrix -> Runtime-Matrix
Migration Modernisierung planen Java 11/17/21 -> Refactoring -> Runtime 2026 -> Dependency-Governance
Referenz Nachschlagen Glossar -> Patterns -> Annotationen -> Maven Plugins -> Fehlerkatalog

12 kompakte Praxis-Sprints

Sprint Ziel Artefakt
1 Paket öffnen und Struktur verstehen eigene Datei-Landkarte
2 Java-11/17/21-Unterschiede fachlich erklären Vergleichstabelle
3 Hexagonal Architecture im Code markieren Ports-/Adapter-Liste
4 Order Aggregate analysieren UML-Skizze
5 REST-Flow erklären Sequenzdiagramm
6 Transaktionsgrenzen bestimmen Notiz zu Konsistenz
7 Outbox Pattern nachvollziehen Ereignisfluss
8 Refactoring-Fallstudie durcharbeiten Vorher/Nachher-Vergleich
9 Virtual Threads bewerten Einsatz-/Nicht-Einsatz-Liste
10 Framework-Labs vergleichen Entscheidungsmatrix
11 Runtime-Modernisierung planen Upgrade-Plan
12 Abschlussprüfung wiederholen Check-Protokoll

Konkrete Code-Aufgaben

java
// Aufgabe: Neue Preisstrategie ergänzen
// Pattern: Strategy Pattern - austauschbare Berechnungsregel
public final class VipPricingPolicy implements PricingPolicy {
    @Override
    public Money calculate(Order order) {
        return order.total().discountPercent(10);
    }
}
text
Aufgabe: Dokumentiere danach in docs/design-patterns.md:
- Pattern: Strategy Pattern
- Zweck: Preislogik austauschbar machen
- Einsatzort: PricingPolicy / VipPricingPolicy
- Begründung: keine if/else-Kaskade im Use Case

Architektur-Aufgaben

text
[REST Adapter] -> [Use Case] -> [Domain Model]
      |              |              |
      v              v              v
 [DTO Mapper]   [Ports]       [Events]
                    |
                    v
          [Repository / Gateway Adapter]

Arbeite heraus:

  • Welche Klassen gehören zum Kern?
  • Welche Klassen sind technische Adapter?
  • Wo dürfen Framework-Annotationen vorkommen?
  • Welche Abhängigkeiten wären in einem echten Projekt kritisch?

Prüf-Aufgaben

Nutze diese Abschlussfragen:

  1. Kannst du den Unterschied zwischen Java 11, 17 und 21 im Enterprise-Kontext erklären?
  2. Kannst du begründen, warum der Core frameworkfrei bleiben sollte?
  3. Kannst du den Outbox-Flow fachlich und technisch erklären?
  4. Kannst du sagen, wo Transaktionen beginnen und enden sollten?
  5. Kannst du mindestens fünf verwendete Entwurfsmuster im Code zeigen?
  6. Kannst du erklären, warum Framework-Adapter separat geprüft werden?
  7. Kannst du eine realistische Upgrade-Reihenfolge für eine Altanwendung formulieren?
Java-GrundlagenJava 11, 17 und 21, Nebenläufigkeit und Migration.
Java-GrundlagenJava 11, 17 und 21, Nebenläufigkeit und Migration.

Enterprise Java 11-21: Lese- und Referenzbuch

Java 11 Java 17 Java 21 Enterprise Architektur Komplexe Codebeispiele

Dieses Paket ist als Lese- und Referenzbuch gebaut: Es erklärt die wichtigsten Konzepte verständlich, zeigt technische und fachliche Diagramme, enthält größere Codebeispiele und verweist auf ein kleines Maven-Beispielprojekt. Der Schwerpunkt liegt nicht auf oberflächlichen Featurelisten, sondern auf der Frage: Wie nutzt man Java 11, 17 und 21 sinnvoll in echten Enterprise-Systemen?

Nutzung des Pakets

Starte mit lernweg.html oder index.html. Für längeres Lesen ist die PDF geeignet; für Nachschlagen und Code ist HTML angenehmer. Die einzelnen Themenbücher sind absichtlich getrennt, damit Navigation, Suche und mobile Nutzung übersichtlich bleiben.

Gesamtlandkarte

Landkarte von Java-Versionen zu Enterprise-Praxis

Landkarte von Java-Versionen zu Enterprise-Praxis

Zielbild des Buchs

Das Buch beantwortet vier zentrale Fragen:

  1. Welche Java-Features aus 11, 17 und 21 sind für Enterprise-Systeme wirklich relevant?
  2. Wie entwirft man wartbare Architekturen mit klaren Grenzen zwischen API, Anwendung, Domäne und Infrastruktur?
  3. Wie sehen komplexere Codebeispiele aus, die nicht nur Spielzeug sind?
  4. Wie migriert man ältere Java-8- oder Java-EE-Systeme schrittweise und risikoarm?

Java 11 als stabile Modernisierungsbasis

Java 11 ist für viele Unternehmen der erste große Schritt weg von Java 8. Der Nutzen liegt weniger in spektakulären Sprachfeatures, sondern in der Stabilisierung der Plattform. Man bekommt eine modernere JVM, bessere Container-Erkennung, einen standardisierten HTTP Client und eine Basis, auf der moderne Libraries wieder sinnvoll gepflegt werden können.

Typische Enterprise-Entscheidung: Java 11 zuerst als technische Plattform stabilisieren, ohne sofort alle Architekturentscheidungen umzubauen. Das reduziert Risiko. Erst wenn Build, Tests, Deployment und Observability zuverlässig sind, sollte man größere Refactorings beginnen.

java
// Java 11: Standard HTTP Client statt alter proprietärer Clients.
// Einsatzort: Adapter zu externem Payment-System.
// Pattern: Adapter Pattern - kapselt das externe HTTP-Protokoll.
HttpClient client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(2))
        .build();

HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://payment.example.test/authorize"))
        .timeout(Duration.ofSeconds(3))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(payload))
        .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 500) {
    throw new ExternalSystemUnavailableException("Payment temporarily unavailable");
}

Java 17 als moderne Enterprise-LTS-Basis

Java 17 ist im Enterprise-Kontext besonders interessant, weil es moderne Sprachmittel mit LTS-Stabilität verbindet. Records eignen sich sehr gut für unveränderliche DTOs, Value Objects und Query-Projektionen. Sealed Classes helfen bei fachlich geschlossenen Modellbereichen, etwa bei Zahlungsentscheidungen oder Fehlerarten.

java
// Java 17: Record als unveränderliches DTO.
// Zweck: API-Daten transportieren, ohne versehentliche Mutation.
public record OrderSummaryDto(
        UUID orderId,
        String customerNumber,
        BigDecimal totalAmount,
        String currency,
        String status
) {}
java
// Java 17: Sealed Interface für kontrollierte fachliche Ergebnisse.
// Pattern: Result Pattern - explizite fachliche Alternativen statt null/Exception-Missbrauch.
public sealed interface PaymentDecision permits Authorized, Rejected, RequiresManualReview {}

public record Authorized(String authorizationId) implements PaymentDecision {}
public record Rejected(String reasonCode, String message) implements PaymentDecision {}
public record RequiresManualReview(String ticketId) implements PaymentDecision {}

Java 21 und moderne Nebenläufigkeit

Java 21 ist für Enterprise-Systeme vor allem wegen Virtual Threads spannend. Viele Geschäftsanwendungen warten sehr häufig auf Datenbank, HTTP, Dateisystem oder Messaging. Virtual Threads können hier ein einfacheres synchrones Programmiermodell ermöglichen. Sie ersetzen aber keine saubere Architektur, keine Pool-Kontrolle und keine Datenbankoptimierung.

Virtual Threads vereinfachen viele blockierende Abläufe, lösen aber keine Datenbankengpässe

Virtual Threads vereinfachen viele blockierende Abläufe, lösen aber keine Datenbankengpässe

java
// Java 21: Virtual Threads für viele blockierende I/O-Aufgaben.
// Einsatzort: Parallelisierung unabhängiger Lieferantenprüfungen.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<InventoryResult> inventory = executor.submit(() -> inventoryClient.check(order));
    Future<RiskResult> risk = executor.submit(() -> riskClient.evaluate(order));
    Future<ContractResult> contract = executor.submit(() -> contractClient.validate(order));

    return new PreCheckResult(inventory.get(), risk.get(), contract.get());
}

Architekturprinzip: Ports, Adapter und Domäne

Eine robuste Enterprise-Anwendung trennt nicht nach technischen Schichten allein, sondern nach Verantwortungen. REST Controller, Datenbankzugriff und externe APIs sind Adapter. Die fachlichen Use Cases liegen im Application Core. Das Domain Model enthält Regeln und Invarianten.

Hexagonale Architektur: technische Adapter außen, fachlicher Kern innen

Hexagonale Architektur: technische Adapter außen, fachlicher Kern innen

Vorteile:

  • Fachliche Logik wird testbar, ohne Webserver oder Datenbank.
  • Externe Systeme können ausgetauscht werden.
  • Migrationen werden einfacher, weil Adapter einzeln ersetzt werden können.
  • Komplexität wird nicht unsichtbar, sondern gezielt eingegrenzt.
java
// Port im Application Core.
// Pattern: Port and Adapter / Hexagonal Architecture.
public interface PaymentPort {
    PaymentDecision authorize(Order order);
}

// Use Case hängt nur am Port, nicht am konkreten HTTP Client.
public final class PlaceOrderUseCase {
    private final OrderRepository orderRepository;
    private final PaymentPort paymentPort;
    private final OutboxPublisher outboxPublisher;

    public PlaceOrderUseCase(OrderRepository orderRepository,
                             PaymentPort paymentPort,
                             OutboxPublisher outboxPublisher) {
        this.orderRepository = orderRepository;
        this.paymentPort = paymentPort;
        this.outboxPublisher = outboxPublisher;
    }

    public UUID place(PlaceOrderCommand command) {
        Order order = Order.create(command.customerNumber(), command.lines());
        PaymentDecision decision = paymentPort.authorize(order);
        order.apply(decision);
        orderRepository.save(order);
        outboxPublisher.stage(order.pullDomainEvents());
        return order.id();
    }
}

Fachliches Beispiel: Order, Billing und Payment

Die Beispiel-Domäne enthält genug Komplexität, um echte Enterprise-Fragen zu behandeln:

Teilbereich Fachliche Verantwortung Technische Herausforderung
Order Bestellung erfassen, Positionen validieren Aggregate, Invarianten, Transaktionen
Payment Zahlung autorisieren oder ablehnen externe API, Timeouts, Fehlerklassen
Billing Rechnung erzeugen Konsistenz, Event-Verarbeitung
Inventory Bestand prüfen Race Conditions, Reservierungen
Notification Kunde informieren asynchron, idempotent

Order Aggregate mit Value Object und Domain Event

Order Aggregate mit Value Object und Domain Event

Outbox Pattern für zuverlässige Events

Das Outbox Pattern löst ein klassisches Problem: Die Anwendung muss Daten in der Datenbank ändern und gleichzeitig ein Event veröffentlichen. Ohne Outbox kann die Datenbankänderung erfolgreich sein, aber das Event verloren gehen. Mit Outbox wird die Event-Absicht in derselben Datenbanktransaktion gespeichert.

Outbox Pattern: Datenänderung und Event-Absicht in einer Transaktion

Outbox Pattern: Datenänderung und Event-Absicht in einer Transaktion

java
// Pattern: Outbox Pattern.
// Zweck: Datenänderung und Event-Publikation zuverlässig entkoppeln.
@Transactional
public UUID placeOrder(PlaceOrderCommand command) {
    Order order = Order.create(command.customerNumber(), command.lines());
    repository.save(order);

    for (DomainEvent event : order.pullDomainEvents()) {
        outboxRepository.insert(OutboxMessage.from(event));
    }
    return order.id();
}

Refactoring einer Monster-Methode

Ein häufiges Legacy-Problem ist eine Methode, die fachliche Regeln, technische Integration, Logging, Transaktionen und Fehlerbehandlung vermischt. Der erste Schritt ist nicht, alles auf einmal neu zu schreiben. Besser ist: Tests ergänzen, Verantwortungen markieren, pure fachliche Logik extrahieren, danach technische Adapter isolieren.

Vorher:

java
public InvoiceResult processOrder(OrderRequest request) {
    // Validierung, Kundensuche, Rabattberechnung, Payment, Rechnung,
    // Datenbankzugriff, Logging, Retry und Mailversand in einer Methode.
    // Problem: schwer testbar, schwer migrierbar, keine klare Verantwortlichkeit.
    return null;
}

Nachher:

java
// Pattern: Facade Pattern im Application Service.
// Zweck: extern einfacher Use Case, intern klare Fachschritte.
public InvoiceResult processOrder(OrderRequest request) {
    Order order = orderFactory.createFrom(request);       // Factory Pattern
    pricingService.calculate(order);                      // Domain Service
    PaymentDecision payment = paymentPort.authorize(order); // Port/Adapter
    order.apply(payment);
    Invoice invoice = invoiceService.createInvoice(order);
    outboxPublisher.stage(order.pullDomainEvents());       // Outbox Pattern
    return InvoiceResult.created(order.id(), invoice.number());
}

Datenzugriff: JPA, Transaktionen und Locking

JPA ist mächtig, aber gefährlich, wenn man es als unsichtbare Magie behandelt. Enterprise-Code sollte klar dokumentieren, welche Aggregate geladen werden, wo Transaktionsgrenzen liegen und welches Locking genutzt wird.

java
// Pattern: Repository Pattern.
// Zweck: fachliche Persistenzschnittstelle statt verstreuter EntityManager-Zugriffe.
public interface OrderRepository {
    Optional<Order> findById(OrderId id);
    void save(Order order);
}
java
// Beispiel: Optimistic Locking in einer Entity.
@Entity
public class OrderEntity {
    @Id
    private UUID id;

    @Version
    private long version;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;
}

REST APIs und Fehlerverträge

Eine Enterprise-REST-API braucht stabile Verträge. Ein sauberer Fehlervertrag ist wichtiger als viele Endpunkte. Fachliche Fehler sollten für Clients verständlich sein, technische Fehler sollen nicht interne Details leaken.

json
{
  "errorCode": "PAYMENT_REJECTED",
  "message": "Die Zahlung wurde vom Zahlungsdienst abgelehnt.",
  "correlationId": "6f378c21-0ad7-4cf7-8f1e-d8040f7f9d0a",
  "details": [
    { "field": "paymentMethod", "reason": "CARD_EXPIRED" }
  ]
}

Testing-Pyramide

Testing-Pyramide für Enterprise Java

Testing-Pyramide für Enterprise Java

Gute Tests sind nicht nur viele Tests. Gute Tests liegen auf der richtigen Ebene. Domain-Regeln gehören in schnelle Unit Tests. Datenbank-Mapping, Transaktionen und externe Adapter gehören in Integrationstests. End-to-End-Tests sollen wenige, aber wichtige Geschäftsflüsse absichern.

java
@Test
void rejectsOrderWhenTotalIsNegative() {
    assertThrows(IllegalArgumentException.class, () ->
        Order.create("C-1000", List.of(new OrderLine("SKU-1", -1, Money.eur("10.00"))))
    );
}

Migration Java 8 zu 11, 17 und 21

Migration von Java 8 nach Java 11, 17 und 21

Migration von Java 8 nach Java 11, 17 und 21

Eine seriöse Migration sollte in Schritten erfolgen:

  1. Build reproduzierbar machen.
  2. Tests und Smoke Checks ergänzen.
  3. Dependencies aktualisieren.
  4. Java 11 als technische Baseline herstellen.
  5. Java 17 für moderne Sprache und LTS-Basis einsetzen.
  6. Java 21 gezielt für Concurrency-Verbesserungen prüfen.

Referenz: Wann welches Feature verwenden?

Feature Version Gut für Vorsicht bei
HTTP Client 11 externe REST/SOAP-nahe Adapter Timeout- und Retry-Strategie fehlt oft
Records 16/17 DTOs, Value Objects, Query Models nicht für JPA-Entities missbrauchen
Sealed Classes 17 geschlossene fachliche Alternativen nicht verwenden, wenn Erweiterbarkeit extern nötig ist
Pattern Matching 17+ lesbarere Fallunterscheidung keine komplexe Businesslogik im switch verstecken
Virtual Threads 21 viele blockierende I/O-Flows DB-Pools und Limits bleiben wichtig

Nächste Ausbaustufen

Diese Version ist ein solides Startpaket. Sinnvolle nächste Erweiterungen wären:

  • Framework-Bücher zu Spring Boot, Jakarta EE, Quarkus und Micronaut.
  • Noch größere Refactoring-Fallstudie mit altem Java-8-Code und modernem Java-17/21-Zielcode.
  • Container-Stack mit PostgreSQL, Kafka/Redpanda und Testcontainers.
  • Mehr UML-Diagramme und Sequenzdiagramme für echte REST- und Event-Flows.
Java 21 Concurrency & Integration

Java 21 Concurrency und Framework-Integration

Warum dieses Kapitel ergänzt wurde

Java 21 macht Virtual Threads für viele Enterprise-Szenarien interessant. Gleichzeitig darf man Virtual Threads nicht als Ersatz für gute Architektur, Datenbankoptimierung oder Backpressure missverstehen.

Klassischer Fehler

java
ExecutorService pool = Executors.newFixedThreadPool(50);
for (OrderRequest request : requests) {
    pool.submit(() -> remotePaymentClient.authorize(request));
}

Das Problem ist nicht nur der Threadpool. Das Problem ist häufig die fehlende fachliche Grenze: Payment, Inventory, Invoice und Outbox werden als technische Calls betrachtet statt als kontrollierter Prozess.

Besseres Denkmodell

java
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    var futures = orderIds.stream()
        .map(id -> executor.submit(() -> fulfillmentSaga.fulfill(id)))
        .toList();

    for (var future : futures) {
        future.get();
    }
}

Enterprise-Regeln

Regel Begründung
Virtual Threads für blockierende I/O prüfen besonders bei vielen parallelen HTTP-/DB-Wartezeiten interessant
Connection Pool bleibt begrenzt Virtual Threads erzeugen keine Datenbankverbindungen
Timeouts bleiben Pflicht blockierende Calls dürfen nicht unendlich warten
Backpressure bleibt Pflicht ein schneller Aufrufer darf Zielsysteme nicht überfahren
Idempotenz bleibt Pflicht Retry ohne Idempotenz erzeugt doppelte Buchungen

Framework-Bezug

Framework Wichtige Frage
Spring Boot Unterstützt der konkrete Stack Virtual Threads sauber, inklusive Security Context und Observability?
Jakarta EE Wie passt Virtual-Thread-Nutzung zum Container-Lifecycle und zu Managed Executors?
Quarkus Blocking vs non-blocking Endpoints sauber unterscheiden
Micronaut Netty/Event-Loop nicht durch Blocking Code blockieren

Fazit

Virtual Threads sind ein Runtime-Werkzeug. Der fachliche Prozessschnitt bleibt wichtiger als die Thread-Technik.

Migration Java 8 bis 21

Migration Java 8 nach 11, 17 und 21

Migration ist ein Risikoprojekt. Man sollte sie nicht mit Feature-Modernisierung, Architekturumbau und Framework-Wechsel gleichzeitig vermischen.

Empfohlener Migrationspfad

Empfohlener Migrationspfad

Phase 1: Inventarisieren

  • Java-Versionen
  • Maven/Gradle Plugins
  • Frameworks
  • Application Server
  • Datenbanktreiber
  • JAXB/JAX-WS/Jakarta-Abhängigkeiten
  • Testabdeckung
  • kritische Batchjobs

Phase 2: Java 11 Baseline

Ziel: Anwendung baut, startet und besteht Smoke Tests.

Phase 3: Java 17 Modernisierung

Ziel: Lesbarkeit, DTOs, Result-Typen, klare Fehlerbehandlung.

Phase 4: Java 21 gezielt einsetzen

Ziel: Virtual Threads oder neue Runtime nur dort einsetzen, wo Messdaten Nutzen zeigen.

Refactoring-Beispiel

java
// Vorher: technische und fachliche Logik vermischt.
if (status.equals("P") && amount.compareTo(BigDecimal.ZERO) > 0 && customer != null) {
    // payment, invoice, mail, db update
}
java
// Nachher: fachliche Sprache sichtbar.
if (order.isPayable()) {
    paymentWorkflow.authorizeAndInvoice(order);
}
Java 11 Referenz

Java 11 Enterprise Referenz

Java 11 ist in vielen Unternehmen die erste realistische Modernisierungsstufe nach Java 8. Das Ziel ist nicht, jede neue Sprachfunktion auszureizen, sondern eine stabile technische Plattform für aktuelle Libraries, moderne Build-Pipelines und bessere Laufzeitdiagnose zu schaffen.

Wichtige Enterprise-Themen

Thema Bedeutung
Standard HTTP Client weniger proprietäre Client-Bibliotheken für einfache Integrationen
Container-Awareness bessere JVM-Erkennung von CPU- und Memory-Grenzen
var im lokalen Kontext weniger Boilerplate, aber nur bei klarer Lesbarkeit
Flight Recorder bessere Diagnose von Performanceproblemen
TLS/Runtime-Modernisierung sicherere Plattform als viele alte Java-8-Runtimes

HTTP Client als Adapter

java
// Pattern: Adapter Pattern.
// Der PaymentHttpAdapter versteckt HTTP-Details vor dem Application Core.
public final class PaymentHttpAdapter implements PaymentPort {
    private final HttpClient client;
    private final URI endpoint;

    public PaymentHttpAdapter(URI endpoint) {
        this.client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(2))
                .build();
        this.endpoint = endpoint;
    }

    @Override
    public PaymentDecision authorize(Order order) {
        String payload = PaymentJsonMapper.toJson(order);
        HttpRequest request = HttpRequest.newBuilder(endpoint.resolve("/payments/authorize"))
                .timeout(Duration.ofSeconds(3))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(payload))
                .build();
        try {
            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
            return PaymentJsonMapper.toDecision(response.body());
        } catch (IOException e) {
            throw new ExternalSystemUnavailableException("Payment I/O failed", e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new ExternalSystemUnavailableException("Payment call interrupted", e);
        }
    }
}

var richtig verwenden

var ist keine Einladung, Typen zu verstecken. Es ist gut, wenn der rechte Ausdruck den Typ klar macht. Es ist schlecht, wenn fachliche Bedeutung verloren geht.

java
// Gut: Typ ist rechts klar sichtbar.
var total = Money.eur("42.90");
var customerId = CustomerId.from("C-1000");

// Schlecht: Der Leser muss raten, was load() zurückgibt.
var result = repository.load(id);

Migrationshinweis

Beim Wechsel von Java 8 nach 11 entstehen typische Probleme: entfernte Java-EE-Module, alte JAXB-Abhängigkeiten, alte Bytecode-Bibliotheken, veraltete Maven-Plugins und implizite Classpath-Annahmen. Deshalb sollte ein Migrationsbranch zuerst Build und Tests stabilisieren, bevor Code modernisiert wird.

Java 17 Referenz

Java 17 Enterprise Referenz

Java 17 ist für Enterprise-Systeme eine sehr starke Modernisierungsbasis: LTS, moderne Sprache, bessere Lesbarkeit und sinnvolle Modellierungsmöglichkeiten. Besonders wichtig sind Records, sealed types, pattern matching und die Möglichkeit, alte mutable DTOs und unklare Ergebnistypen zu verbessern.

Records für DTOs und Value Objects

java
// Java 17: unveränderliches Command-Objekt.
public record PlaceOrderCommand(
        String customerNumber,
        List<OrderLineCommand> lines,
        String paymentMethod
) {
    public PlaceOrderCommand {
        if (customerNumber == null || customerNumber.isBlank()) {
            throw new IllegalArgumentException("customerNumber is required");
        }
        lines = List.copyOf(lines);
    }
}

Sealed Classes für fachliche Alternativen

java
// Pattern: Result Pattern.
// Zweck: fachlich erwartete Alternativen typisiert statt null/Exception.
public sealed interface OrderPlacementResult
        permits OrderPlaced, OrderRejected, OrderRequiresManualReview {
}

public record OrderPlaced(UUID orderId, String invoiceNumber) implements OrderPlacementResult {}
public record OrderRejected(String reasonCode, String message) implements OrderPlacementResult {}
public record OrderRequiresManualReview(UUID orderId, String ticketNumber) implements OrderPlacementResult {}

Switch über fachliche Ergebnisse

java
public ApiResponse toResponse(OrderPlacementResult result) {
    return switch (result) {
        case OrderPlaced placed -> ApiResponse.created(placed.orderId());
        case OrderRejected rejected -> ApiResponse.badRequest(rejected.reasonCode(), rejected.message());
        case OrderRequiresManualReview review -> ApiResponse.accepted(review.ticketNumber());
    };
}

Enterprise-Regel

Records sind hervorragend für DTOs, Commands und Value Objects. Für JPA-Entities sind klassische Klassen meist weiterhin sinnvoller, weil JPA Identität, Lifecycle, Lazy Loading und Proxies verwaltet.

Java 21 Referenz

Java 21 Enterprise Referenz

Java 21 bringt mit Virtual Threads eine besonders wichtige Verbesserung für klassische Enterprise-Anwendungen, die viele blockierende I/O-Aufgaben ausführen. Der wichtigste Punkt: Virtual Threads machen blockierenden Code skalierbarer, aber nicht automatisch richtig.

Klassisches Problem

In vielen Anwendungen blockieren Plattform-Threads beim Warten auf Datenbank, REST APIs oder Dateisystem. Große Threadpools erhöhen Speicherverbrauch und Kontextwechsel. Zu kleine Pools erzeugen Warteschlangen.

Virtual Threads

java
// Java 21: viele blockierende Tasks mit einfachem synchronen Code.
public final class ParallelPreCheckService {
    public PreCheckResult execute(Order order) {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            var inventory = executor.submit(() -> checkInventory(order));
            var fraud = executor.submit(() -> checkFraud(order));
            var contract = executor.submit(() -> checkContract(order));
            return new PreCheckResult(inventory.get(), fraud.get(), contract.get());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new PreCheckFailedException("Pre-check interrupted", e);
        } catch (ExecutionException e) {
            throw new PreCheckFailedException("Pre-check failed", e.getCause());
        }
    }
}

Was Virtual Threads nicht lösen

Problem Warum bleibt es bestehen?
Zu kleiner DB-Pool Virtual Threads erzeugen mehr Nachfrage, aber keine neuen DB-Verbindungen
Langsame Queries schlechte Indizes bleiben schlecht
Fehlende Timeouts mehr parallele Wartezeit kann Ausfälle verstärken
Schlechte Transaktionsgrenzen Locks und Deadlocks verschwinden nicht

Empfehlung

Virtual Threads zuerst an klar abgegrenzten I/O-Flows testen: parallele externe Checks, Reporting-Aufgaben oder Batch-ähnliche Verarbeitung. Danach Metriken vergleichen: Latenz, Durchsatz, DB-Pool-Auslastung, Fehlerquote und Timeout-Verhalten.

Architektur & EntwicklungArchitektur, Build, Refactoring, Beispiele und Entwurfsmuster.
Enterprise-Architektur

Enterprise Architektur mit Java

Enterprise-Architektur bedeutet nicht, möglichst viele Schichten zu zeichnen. Gute Architektur macht fachliche Regeln sichtbar, technische Abhängigkeiten kontrollierbar und Änderungskosten geringer.

Schichtenmodell und Grenzen

Klassische Layered Architecture ist nützlich, wenn sie diszipliniert umgesetzt wird. Controller dürfen keine Geschäftsregeln enthalten. Repositories dürfen keine fachlichen Entscheidungen treffen. Domain Services dürfen nicht direkt HTTP oder SQL sprechen.

text
Client
  -> REST Controller
      -> Application Service / Use Case
          -> Domain Model
          -> Ports
              -> Adapter: JPA, HTTP, Kafka

Hexagonale Architektur

Ports und Adapter schützen den fachlichen Kern

Ports und Adapter schützen den fachlichen Kern

Beispielhafte Paketstruktur

text
com.example.orderbilling
├── order
│   ├── domain
│   ├── application
│   └── adapter
│       ├── rest
│       ├── persistence
│       └── messaging
├── billing
└── shared

Architekturregeln

Regel Begründung
Domain kennt keine Frameworks fachliche Tests bleiben schnell und stabil
Application kennt Ports Use Cases bleiben gegen Adapter austauschbar
Adapter kennen Frameworks technische Details bleiben außen
DTOs sind API-Verträge Domain wird nicht direkt nach außen geleakt

Anti-Pattern: Anemic Service Blob

Ein häufiger Fehler ist ein riesiger Service, der alle fachlichen Regeln enthält, während Entities nur Datencontainer sind. Besser ist, Invarianten in Aggregate und Value Objects zu verlagern.

Komplexe Codebeispiele

Komplexe Codebeispiele

Dieses Kapitel sammelt größere zusammenhängende Beispiele. Die vollständigen Dateien liegen zusätzlich im Ordner code/enterprise-order-billing-demo.

Use Case: Bestellung anlegen

java
public UUID place(PlaceOrderCommand command) {
    Order order = orderFactory.createFrom(command);
    pricingService.applyPrice(order);
    PaymentDecision decision = paymentPort.authorize(order);
    order.apply(decision);
    orderRepository.save(order);
    outboxPublisher.stage(order.pullDomainEvents());
    return order.id();
}

Ergebnis-Typen statt null

java
public sealed interface PaymentDecision permits Authorized, Rejected, RequiresManualReview {}
public record Authorized(String authorizationId) implements PaymentDecision {}
public record Rejected(String reasonCode, String message) implements PaymentDecision {}
public record RequiresManualReview(String ticketId) implements PaymentDecision {}

ASCII-Sequenz

text
Client
  -> OrderController
      -> PlaceOrderUseCase
          -> OrderFactory
          -> PricingService
          -> PaymentPort
              -> PaymentGatewayAdapter
          -> OrderRepository
          -> OutboxPublisher
Maven Enterprise Build

Maven Enterprise Build und Plugin-Referenz

Maven ist in Enterprise-Java nicht nur ein Build-Tool. Es ist ein Governance-Werkzeug: Dependency-Versionen, Plugin-Versionen, Java-Release, Testphasen, Packaging, Lizenzprüfung und Architekturgrenzen werden reproduzierbar.

Multi-Module Maven als Architekturstütze.

Multi-Module Maven als Architekturstütze.

Parent-POM-Regel

xml
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example.enterprisejava</groupId>
  <artifactId>enterprise-platform-parent</artifactId>
  <version>1.0.0</version>
  <packaging>pom</packaging>

  <properties>
    <maven.compiler.release>21</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencyManagement>
    <dependencies>
      <!-- zentral kontrollierte Versionen -->
    </dependencies>
  </dependencyManagement>
</project>

Wichtige Plugins

Plugin Zweck
maven-compiler-plugin Java-Version reproduzierbar setzen
maven-surefire-plugin Unit Tests ausführen
maven-failsafe-plugin Integration Tests trennen
maven-enforcer-plugin Versionen und Regeln erzwingen
dependency-check / OWASP bekannte Sicherheitslücken prüfen
jacoco-maven-plugin Coverage messen
flatten-maven-plugin saubere veröffentlichbare POMs
versions-maven-plugin Updates analysieren

Empfehlung

Ein Enterprise-Build sollte fehlschlagen, wenn die Java-Version falsch ist, verbotene Dependencies auftauchen, Tests nicht laufen oder Plugin-Versionen implizit bleiben.

Refactoring-Fallstudie Java 8 modern

Refactoring-Fallstudie: Java-8-Monster-Methode modernisieren

Dieses Kapitel zeigt eine typische Modernisierung: Aus einer alten Methode mit vielen Verantwortungen entsteht ein klarer Use Case mit Domäne, Ports und Adaptern.

Monster-Methode wird in Verantwortlichkeiten zerlegt.

Monster-Methode wird in Verantwortlichkeiten zerlegt.

Ausgangsproblem

java
public InvoiceResult process(OrderRequest request) {
    if (request == null) throw new IllegalArgumentException("request missing");
    if (request.customerId() == null) throw new IllegalArgumentException("customer missing");
    BigDecimal total = BigDecimal.ZERO;
    for (OrderLineRequest line : request.lines()) {
        if (line.quantity() <= 0) throw new IllegalArgumentException("quantity invalid");
        total = total.add(line.price().multiply(BigDecimal.valueOf(line.quantity())));
    }
    if (request.coupon() != null) total = total.multiply(new BigDecimal("0.90"));
    PaymentResult payment = paymentClient.authorize(request.customerId(), total);
    if (!payment.approved()) return InvoiceResult.rejected(payment.reason());
    long invoiceId = invoiceDao.insert(request.customerId(), total, payment.id());
    eventPublisher.publish("invoice.created", invoiceId);
    mailClient.sendInvoice(invoiceId);
    return InvoiceResult.created(invoiceId);
}

Probleme

Problem Folge
Validierung, Preislogik, Payment, DB und Mail in einer Methode kaum testbar
technische Exceptions für fachliche Fälle schlechte Fehlerbehandlung
direkter Zugriff auf externe Systeme keine sauberen Tests
kein Domain-Modell Regeln verteilen sich beliebig

Zielstruktur

java
// Pattern: Facade / Application Service - ein klarer Eingang für den fachlichen Ablauf.
public final class CreateInvoiceUseCase {
    private final OrderValidator validator;
    private final PricingPolicy pricing;
    private final PaymentPort paymentPort;
    private final InvoiceRepository invoiceRepository;
    private final OutboxPort outboxPort;

    public InvoiceResult create(CreateInvoiceCommand command) {
        ValidatedOrder order = validator.validate(command);
        Money total = pricing.calculate(order);
        PaymentDecision decision = paymentPort.authorize(order.customerId(), total);
        Invoice invoice = Invoice.create(order, total, decision);
        invoiceRepository.save(invoice);
        outboxPort.store(new InvoiceCreatedEvent(invoice.id(), total));
        return InvoiceResult.created(invoice.id());
    }
}

Refactoring-Reihenfolge

  1. Tests um aktuelles Verhalten legen.
  2. reine Berechnungen extrahieren.
  3. Ports für externe Systeme einführen.
  4. fachliche Ergebnisse explizit modellieren.
  5. Datenbanktransaktion und Outbox klären.
  6. Controller/API getrennt anpassen.
  7. Monitoring und Fehlercodes ergänzen.
Design Patterns

Design Patterns im Enterprise-Java-Paket

Diese Datei dokumentiert die verwendeten Entwurfsmuster. Im Maven-Beispielprojekt sind die wichtigsten Pattern zusätzlich direkt im Code kommentiert.

Pattern Zweck Einsatzort Begründung
Repository Pattern Persistenz fachlich kapseln OrderRepository Domain/Application soll keinen EntityManager kennen
Adapter Pattern externe Systeme kapseln PaymentGatewayAdapter Payment-API kann gewechselt werden
Facade Pattern Use Case nach außen einfach halten PlaceOrderUseCase REST muss keine Fachschritte orchestrieren
Factory Pattern komplexe Erzeugung zentralisieren OrderFactory Validierung und Konstruktion bleiben konsistent
Strategy Pattern austauschbare Preislogik PricingStrategy Rabattregeln variieren nach Kundentyp
Outbox Pattern Events zuverlässig veröffentlichen OutboxMessageRepository Datenänderung und Event-Absicht in einer Transaktion
Result Pattern fachliche Alternativen typisiert ausdrücken PaymentDecision Keine null-Rückgaben oder technische Exceptions für erwartete Fälle
Idempotent Consumer doppelte Events sicher behandeln BillingEventHandler Messaging liefert praktisch mindestens einmal
Mapper Pattern API und Domain trennen OrderApiMapper DTOs und Domainmodell entwickeln sich unabhängig
Specification Pattern komplexe Regeln kombinieren OrderSpecification Regeln werden testbar und benennbar

Beispiel: Strategy Pattern

java
public interface PricingStrategy {
    Money calculate(Order order);
}

public final class EnterpriseCustomerPricing implements PricingStrategy {
    public Money calculate(Order order) {
        return order.rawTotal().minusPercent(10);
    }
}
Daten & SchnittstellenJPA, Transaktionen, REST, Messaging und Outbox.
Daten, JPA & Transaktionen

Datenzugriff, JPA und Transaktionen

Datenzugriff ist einer der häufigsten Orte für versteckte Enterprise-Komplexität. Performanceprobleme, Lazy-Loading-Fehler, falsche Transaktionsgrenzen und Locking-Konflikte entstehen oft nicht durch JPA selbst, sondern durch unklare Verantwortungen.

Repository Pattern

java
// Pattern: Repository Pattern.
// Zweck: Fachliche Persistenzschnittstelle statt EntityManager überall.
public interface OrderRepository {
    Optional<Order> findById(OrderId id);
    void save(Order order);
    boolean existsOpenOrderForCustomer(CustomerNumber customerNumber);
}

Optimistic Locking

java
@Entity
@Table(name = "orders")
public class OrderEntity {
    @Id
    private UUID id;

    @Version
    private long version;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;
}

Optimistic Locking ist gut, wenn Konflikte selten sind. Bei hoher Konkurrenz auf denselben Datensatz braucht man andere Strategien: fachliche Reservierungen, Queueing, pessimistische Locks oder eine andere Aggregatgrenze.

Transaktionsgrenzen

Eine Transaktion sollte einen fachlich konsistenten Schritt absichern. Sie sollte nicht unkontrolliert externe HTTP-Aufrufe enthalten. Externe Systeme können hängen, langsam sein oder uneindeutig antworten. Deshalb sind Outbox, Saga und Retry-Strategien in Enterprise-Systemen wichtig.

N+1 Problem

java
// Problematisch: viele Lazy Loads in einer Schleife.
for (Order order : orderRepository.findAllOpen()) {
    System.out.println(order.customer().name());
}

Besser sind klare Queries, Fetch Joins, Projektionen oder Query-Modelle.

REST API Design

REST API Design für Enterprise Java

REST APIs sind Verträge. Ein guter Vertrag bleibt stabil, auch wenn interne Klassen, Datenbanktabellen oder Frameworks gewechselt werden.

DTO statt Entity Leak

java
public record CreateOrderRequest(
        String customerNumber,
        List<CreateOrderLineRequest> lines,
        String paymentMethod
) {}

public record CreateOrderResponse(
        UUID orderId,
        String status,
        List<LinkDto> links
) {}

Fehlervertrag

java
public record ApiError(
        String errorCode,
        String message,
        String correlationId,
        List<FieldError> details
) {}

API Versionierung

Strategie Einsatz
URL-Version gut sichtbar, einfach für große externe Clients
Header-Version sauberer, aber schwerer zu testen
kompatible Erweiterung bevorzugt für kleine additive Änderungen

Controller dünn halten

java
// Pattern: Facade über Application Service.
// Controller übersetzt HTTP, entscheidet aber keine Fachregeln.
public CreateOrderResponse create(CreateOrderRequest request) {
    PlaceOrderCommand command = mapper.toCommand(request);
    OrderPlacementResult result = placeOrderUseCase.place(command);
    return mapper.toResponse(result);
}
Messaging, Outbox & Kafka

Messaging, Kafka und Outbox

Event-getriebene Systeme sind nicht automatisch besser. Sie sind besser, wenn man lose Kopplung, Wiederholbarkeit, Idempotenz und Observability sauber behandelt.

Outbox Pattern

Outbox Pattern als Brücke zwischen Datenbanktransaktion und Event-Publishing

Outbox Pattern als Brücke zwischen Datenbanktransaktion und Event-Publishing

Event Design

java
public record OrderPlacedEvent(
        UUID eventId,
        Instant occurredAt,
        UUID orderId,
        String customerNumber,
        BigDecimal totalAmount,
        String currency
) implements DomainEvent {}

Idempotenter Consumer

java
// Pattern: Idempotent Consumer.
// Zweck: doppelte Events sicher ignorieren.
public void handle(OrderPlacedEvent event) {
    if (processedEventRepository.exists(event.eventId())) {
        return;
    }
    billingService.createInvoiceFor(event.orderId());
    processedEventRepository.markProcessed(event.eventId());
}

Typische Fehler

Fehler Folge
Event enthält zu wenig Kontext Consumer müssen synchron zurückfragen
keine Idempotenz doppelte Rechnungen, doppelte Mails
keine Dead Letter Strategie fehlerhafte Events blockieren Verarbeitung
keine Korrelation Debugging über Services wird schwer
Qualität & BetriebTests, Security, Performance, Observability und Qualitätskontrollen.
Testing Enterprise Java

Testing für Enterprise Java

Testing ist nicht nur Qualitätssicherung, sondern Architekturwerkzeug. Gute Tests erzwingen klare Grenzen und zeigen, ob Domain, Application und Adapter sauber getrennt sind.

Testing-Pyramide mit Domain-, Integrations- und E2E-Tests

Testing-Pyramide mit Domain-, Integrations- und E2E-Tests

Domain Test

java
@Test
void orderCannotBePlacedWithoutLines() {
    var command = new PlaceOrderCommand("C-1000", List.of(), "CARD");
    assertThrows(IllegalArgumentException.class, () -> Order.create(command.customerNumber(), command.lines()));
}

Application Test mit Fake Port

java
// Pattern: Test Double / Fake.
// Zweck: Use Case ohne echtes Payment-System testen.
final class AlwaysAuthorizedPaymentPort implements PaymentPort {
    public PaymentDecision authorize(Order order) {
        return new Authorized("AUTH-1");
    }
}

Integrationstest

Integrationstests prüfen Adapter: JPA-Mapping, SQL, Transaktionen, REST-Serialisierung oder Messaging. Sie sind langsamer, aber unverzichtbar für technische Verträge.

Performance, JVM & Virtual Threads

Performance, JVM und Nebenläufigkeit

Performance in Enterprise Java beginnt selten mit Mikrooptimierung. Meist zählen Datenbankzugriffe, Threading, Garbage Collection, Netzwerk, Serialisierung und unnötige Synchronisation.

Diagnose-Reihenfolge

  1. Metriken prüfen: Latenz, Durchsatz, Fehler, Sättigung.
  2. Logs mit Korrelation auswerten.
  3. Datenbankqueries messen.
  4. Thread Dumps und Flight Recorder nutzen.
  5. Erst dann Code optimieren.

Threadpool vs Virtual Threads

java
// Klassisch: begrenzter Pool, sinnvoll für CPU-lastige Arbeit.
ExecutorService pool = Executors.newFixedThreadPool(16);

// Java 21: Virtual Threads, sinnvoll für viele blockierende I/O-Aufgaben.
ExecutorService vt = Executors.newVirtualThreadPerTaskExecutor();

JVM-Tuning-Regel

Nicht blind Flags kopieren. Erst messen, dann gezielt ändern. Container-Limits, Heap-Größe, GC-Logs und Response-Time-Ziele müssen zusammen betrachtet werden.

Security Enterprise Java

Security in Enterprise Java

Security besteht nicht nur aus Login. Gute Enterprise-Systeme unterscheiden Authentifizierung, technische Autorisierung, fachliche Autorisierung, Audit und Geheimnisverwaltung.

JWT Security Flow.

JWT Security Flow.

Authentifizierung vs Autorisierung

Begriff Frage
Authentifizierung Wer bist du?
Technische Autorisierung Darfst du diesen Endpunkt aufrufen?
Fachliche Autorisierung Darfst du genau diese Aktion auf genau diesem Objekt ausführen?
Audit Können wir später erklären, was passiert ist?

Beispiel: fachliche Policy

java
// Pattern: Policy Pattern - fachliche Berechtigung ist isoliert testbar.
public final class RefundPolicy {
    public boolean mayRefund(UserPrincipal user, Payment payment) {
        if (!user.hasRole("PAYMENT_REFUND")) return false;
        if (!payment.isCaptured()) return false;
        if (payment.isOlderThan(Duration.ofDays(90))) return false;
        return payment.amount().isLessThanOrEqual(user.maxRefundLimit());
    }
}

Typische Fehler

  • Nur Rollen prüfen, aber Objektzustand ignorieren.
  • Token ungeprüft weiterreichen.
  • Secrets in Properties oder Git speichern.
  • Audit-Logs ohne Korrelations-ID schreiben.
  • Security erst nachträglich in fertige Flows einbauen.
Observability, Logging & Tracing

Observability: Logging, Metrics und Tracing

Observability beantwortet Betriebsfragen. Ein Enterprise-System ist nicht fertig, wenn es lokal funktioniert. Es muss im Fehlerfall erklärbar sein.

Observability Pipeline.

Observability Pipeline.

Drei Säulen

Säule Zweck Beispiel
Logs konkrete Ereignisse Order 4711 wurde abgelehnt
Metrics aggregierte Messwerte Fehlerrate, Latenz, Durchsatz
Traces Request-Kette Client → Gateway → Order → Payment

Strukturierte Logs

java
// Pattern: Correlation Context - gemeinsame ID für verteilte Analyse.
logger.info("order_placed correlationId={} orderId={} customerId={} amount={}",
        correlationId.value(), order.id(), order.customerId(), order.total());

Gute Metriken

  • Latenz pro API und externer Abhängigkeit.
  • Fehlerquote nach Fehlerklasse.
  • Queue-/Topic-Lag bei Messaging.
  • Datenbankverbindungs-Pool-Auslastung.
  • Business-Metriken wie Bestellungen pro Minute oder Stornoquote.
Fehlerkatalog Enterprise Java

Fehlerkatalog Enterprise Java

Dieser Katalog sammelt typische Fehlerbilder, Ursachen und Gegenmaßnahmen.

Architekturfehler

Fehler Symptom Gegenmaßnahme
Anämisches Domain Model Regeln liegen überall Aggregates und Policies einführen
Controller mit Business Logic API schwer testbar Use Cases extrahieren
Repository entscheidet fachlich Datenzugriff kennt Regeln Repository auf Persistenz beschränken
Adapter-Leakage Domäne kennt HTTP/JPA/Kafka Ports einführen

Datenbankfehler

Fehler Symptom Gegenmaßnahme
N+1 Queries schlechte Performance Fetch-Strategie, Projektionen, Query-Design
lange Transaktionen Locks, Timeouts Transaktionsgrenzen verkleinern
fehlendes Optimistic Locking verlorene Updates @Version nutzen
Lazy Loading im JSON Fehler oder riesige Responses DTOs und Mapper verwenden

Messagingfehler

Fehler Symptom Gegenmaßnahme
Event ohne Idempotenz doppelte Verarbeitung Idempotency Key speichern
DB Commit und Kafka Publish getrennt verlorene Events Outbox Pattern
keine Dead Letter Strategie blockierte Consumer DLQ und Fehlerklassifikation

Java-21-Fehler

Fehler Symptom Gegenmaßnahme
Virtual Threads für CPU-Arbeit keine Verbesserung CPU-Arbeit begrenzen, Pools nutzen
blockierende DB ohne Poolgrenzen DB überlastet Connection Pool bewusst dimensionieren
ThreadLocal unkontrolliert Speicher-/Kontextfehler Kontext explizit modellieren
Abschluss-Checkliste

Abschluss-Checkliste

Kompakte Liste für die finale Kontrolle des Enterprise-Java-Pakets.

Abschluss-Checkliste

Bereich Status Hinweis
Startdateien OK lernweg.html, index.html, README.html vorhanden
Buchdateien OK Masterbuch plus Version 2, 3, 4 und finales Portal-Handbuch
Themenkapitel OK Architektur, REST, JPA, Messaging, Security, Testing, Performance, Migration
Referenz OK Glossar, Patterns, Annotationen, Maven, Fehlerkatalog, Matrizen
Diagramme OK SVG und PNG vorhanden
Code-Labs OK JDK-only Labs kompilierbar
Framework-Labs Hinweis Build benötigt Maven und Internetzugriff
PDF OK finales Portal-PDF gerendert
ZIP OK Manifest und Checksummen erzeugt

Was bewusst nicht behauptet wird

Das Paket ist ein Lern- und Referenzpaket. Es ersetzt keine produktive Security-, Lizenz- oder Architekturfreigabe. Externe Framework-Abhängigkeiten wurden nicht vollständig heruntergeladen, weil die lokale Umgebung keinen Maven-Build mit Repository-Zugriff ausführt.

FrameworksSpring Boot, Jakarta EE, Quarkus, Micronaut und ihre Auswahl.
Enterprise-Praxis

Frameworks, Build, Security und Betrieb

Diese Erweiterung baut auf dem ersten Masterbuch auf. Der Fokus liegt auf Framework-Auswahl, Maven-Struktur, Sicherheitsfluss, Observability, komplexerer Refactoring-Fallstudie und einem erweiterten Java/Maven-Beispielprojekt.

Überblick

Bereich Ergänzung
Frameworks Spring Boot, Jakarta EE, Quarkus, Micronaut im Vergleich
Build Maven Multi-Module, Dependency Management, Plugin-Regeln
Code neues größeres JDK-only Enterprise-Lab mit markierten Patterns
Security JWT/OIDC-Flows, Rollen, fachliche Autorisierung
Observability Logs, Metrics, Traces, Korrelation und Fehleranalyse
Refactoring Monster-Methode zu Use Case, Ports, Domäne und Adapter
Referenz Annotationen, Maven Plugins, Fehlerkatalog

Framework-Landkarte

Frameworks haben unterschiedliche Stärken, aber ähnliche Enterprise-Grundprobleme.

Frameworks haben unterschiedliche Stärken, aber ähnliche Enterprise-Grundprobleme.

Entscheidungsregel

Spring Boot ist oft die pragmatische Standardwahl, wenn Team, Libraries und Betrieb eine breite Plattform brauchen. Jakarta EE ist stark, wenn Standards, Applikationsserver und lange Kompatibilität zentral sind. Quarkus ist interessant bei Cloud-Native, schnellem Start und Kubernetes-Nähe. Micronaut ist attraktiv für schlanke Services, AOT-Ansätze und klare DI-Modelle.

Wichtig ist nicht nur das Framework. Entscheidend ist, ob die Architektur verhindert, dass Controller, Datenbank, Messaging und Domäne zu einem unwartbaren Block verschmelzen.

Maven als Architektur-Werkzeug

Maven-Module erzwingen fachliche Grenzen besser als nur Packages.

Maven-Module erzwingen fachliche Grenzen besser als nur Packages.

Ein Enterprise-Maven-Build sollte nicht nur kompilieren. Er sollte Architekturgrenzen sichtbar machen: Domäne ohne Framework, Anwendung mit Ports, Adapter mit Infrastruktur, Tests mit klaren Schichten.

xml
<modules>
  <module>shared-kernel</module>
  <module>order-domain</module>
  <module>order-application</module>
  <module>adapters</module>
  <module>architecture-tests</module>
</modules>

Spring Boot Request Flow

Ein sauberer Spring Boot Flow hält fachliche Regeln aus dem Controller heraus.

Ein sauberer Spring Boot Flow hält fachliche Regeln aus dem Controller heraus.

java
@RestController
@RequestMapping("/orders")
final class OrderController {
    private final PlaceOrderUseCase useCase;

    OrderController(PlaceOrderUseCase useCase) {
        this.useCase = useCase;
    }

    @PostMapping
    ResponseEntity<OrderResponse> place(@Valid @RequestBody PlaceOrderRequest request) {
        PlaceOrderResult result = useCase.place(request.toCommand());
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(OrderResponse.from(result));
    }
}

Jakarta EE Container Flow

Jakarta EE liefert viele Querschnittsfunktionen über Container und Standards.

Jakarta EE liefert viele Querschnittsfunktionen über Container und Standards.

java
@Path("/orders")
@RequestScoped
public class OrderResource {
    @Inject
    PlaceOrderService service;

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response place(PlaceOrderRequest request) {
        return Response.status(Response.Status.CREATED)
                .entity(service.place(request.toCommand()))
                .build();
    }
}

Security Flow

JWT löst Authentifizierungstransport, ersetzt aber keine fachliche Autorisierung.

JWT löst Authentifizierungstransport, ersetzt aber keine fachliche Autorisierung.

Gute Enterprise-Security trennt vier Ebenen: Identität, technische Zugriffskontrolle, fachliche Berechtigungsregel und Auditierbarkeit. Ein Token sagt nicht automatisch, ob ein Benutzer eine bestimmte Rechnung stornieren darf.

java
// Pattern: Policy / Specification - fachliche Autorisierung wird testbar und zentral.
public final class CancelInvoicePolicy {
    public boolean mayCancel(UserPrincipal user, Invoice invoice) {
        return user.hasRole("BILLING_ADMIN")
                && invoice.isOpen()
                && !invoice.isExportedToAccounting();
    }
}

Observability

Logs, Metrics und Traces beantworten unterschiedliche Betriebsfragen.

Logs, Metrics und Traces beantworten unterschiedliche Betriebsfragen.

Logs erklären konkrete Ereignisse. Metrics zeigen Trends und SLO-Verletzungen. Traces verbinden mehrere Services zu einer Request-Kette. Ohne Korrelations-ID kann ein verteilter Fehler kaum wirtschaftlich analysiert werden.

java
// Pattern: Correlation Context - technische Nachvollziehbarkeit über Systemgrenzen.
public record CorrelationId(String value) {
    public static CorrelationId newId() {
        return new CorrelationId(UUID.randomUUID().toString());
    }
}

Refactoring-Fallstudie

Eine Monster-Methode wird in fachliche Verantwortlichkeiten aufgeteilt.

Eine Monster-Methode wird in fachliche Verantwortlichkeiten aufgeteilt.

Das Ziel ist nicht, möglichst viele Klassen zu erzeugen. Das Ziel ist, Entscheidungen dort zu platzieren, wo sie fachlich hingehören: Validierung nah am Eingang, Regeln in der Domäne, Orchestrierung im Use Case, Infrastruktur hinter Ports.

java
// Vorher: technische und fachliche Verantwortung vermischt.
public InvoiceResult process(OrderRequest request) {
    validateRawRequest(request);
    BigDecimal total = calculatePrice(request);
    reserveInventory(request);
    PaymentResult payment = paymentClient.authorize(request.customerId(), total);
    Long invoiceId = jdbc.insertInvoice(request, total, payment.id());
    mailClient.sendInvoice(invoiceId);
    return new InvoiceResult(invoiceId, "CREATED");
}
java
// Nachher: Use Case orchestriert, Details liegen in spezialisierten Komponenten.
// Pattern: Application Service / Use Case.
public InvoiceResult process(CreateInvoiceCommand command) {
    ValidatedOrder order = validator.validate(command);
    Money total = pricing.calculate(order);
    PaymentDecision decision = paymentPort.authorize(order.customerId(), total);
    Invoice invoice = invoiceFactory.create(order, total, decision);
    invoiceRepository.save(invoice);
    outboxPort.publish(new InvoiceCreatedEvent(invoice.id(), total));
    return InvoiceResult.created(invoice.id());
}

Nächster Lernschritt

Sinnvoll wäre danach eine Version 3 mit noch größeren Framework-spezifischen Beispielprojekten: Spring Boot Runtime, Jakarta EE Variante, Quarkus Runtime, Micronaut Runtime, jeweils mit gleicher Domäne und vergleichbarer Architektur.

Frameworks vergleichen

Vier Frameworks, eine gemeinsame Domäne

Ziel dieser Ausbaustufe

Version 3 ergänzt das bisherige Lese- und Referenzbuch um eine praktische Vergleichsebene: dieselbe Order-Domäne wird in getrennten Projekten mit Spring Boot, Jakarta EE, Quarkus und Micronaut angebunden. Entscheidend ist nicht, welches Framework schöner aussieht, sondern welche Teile stabil, fachlich und langlebig sind.

Version 3: gleicher Core, vier Framework-Adapter

Das zentrale Lernprinzip lautet:

Der Enterprise-Core gehört nicht einem Framework. Frameworks sind Adapter, Runtimes und Integrationsumgebungen.

Paketstruktur der Framework-Labs

text
code/framework-labs/
├── pom.xml
├── order-platform-core/
│   ├── src/main/java/.../domain
│   ├── src/main/java/.../application
│   ├── src/main/java/.../application/port
│   ├── src/main/java/.../adapter
│   └── docs/design-patterns.md
├── spring-boot-order-api/
├── jakarta-ee-order-api/
├── quarkus-order-api/
└── micronaut-order-api/

Maven-Modulgrenzen und Verantwortungen

Warum ein gemeinsamer Core besser ist

Ein häufiger Fehler in Enterprise-Projekten ist, dass Businesslogik direkt in Controller, Resources, Repositories, Entity Listener, Framework Services oder Transaction Scripts wandert. Anfangs fühlt sich das schnell an. Später entsteht Abhängigkeit von Details: Annotationen, Container-Lifecycle, Datenbank-Mapping, HTTP-Struktur, Security-Kontext und Deployment-Modell.

Der gemeinsame Core trennt deshalb bewusst:

Ebene Darf Framework kennen? Beispiel
Domain Model Nein Order, Money, OrderLine
Application Service Nein PlaceOrderUseCase, OrderFulfillmentSaga
Port Nein PaymentGateway, OrderRepository
Adapter Ja Controller, JAX-RS Resource, CDI Producer
Runtime Ja Spring Boot JAR, Jakarta WAR, Quarkus Runner, Micronaut App

Request Flow im Vergleich

Der HTTP-Einstieg unterscheidet sich, aber der Use Case bleibt gleich.

Request Flow im Framework-Vergleich

java
// Kernidee: jeder Adapter erzeugt am Ende denselben Command.
var command = new PlaceOrderCommand(
    new CustomerId(request.customerId()),
    request.lines().stream()
        .map(line -> new PlaceOrderCommand.Line(
            line.sku(),
            line.quantity(),
            Money.eur(line.unitPrice())))
        .toList()
);

var receipt = placeOrderUseCase.place(command);

Spring Boot Lab

Spring Boot wird als Adapterprojekt modelliert. Der Controller ist dünn, Bean-Konfiguration ist bewusst im Adapterprojekt, und die Fehlerbehandlung übersetzt Exceptions in API-Fehler.

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

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

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public OrderResponse place(@Valid @RequestBody OrderRequest request) {
        var command = toCommand(request);
        var receipt = placeOrderUseCase.place(command);
        return toResponse(receipt);
    }
}

Spring Boot ist stark, wenn man viel Ökosystem braucht: Actuator, Security, Batch, Integration, Data, Cloud Config, Observability und eine sehr breite Dokumentationsbasis. Die Gefahr ist, dass Fachlogik zu leicht in @Service-Klassen landet, die eigentlich nur Framework-Services statt Use Cases sind.

Jakarta EE Lab

Jakarta EE zeigt ein anderes Betriebsmodell: WAR, Application Server, JAX-RS, CDI und gemanagte Infrastruktur. Das ist besonders relevant, wenn bestehende Java-EE-/Jakarta-EE-Landschaften modernisiert werden, ohne sofort alles in Boot-Anwendungen umzubauen.

java
@Path("/orders")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class OrderResource {
    @Inject PlaceOrderUseCase placeOrderUseCase;

    @POST
    public Response place(OrderRequest request) {
        var receipt = placeOrderUseCase.place(toCommand(request));
        return Response.status(Response.Status.CREATED)
            .entity(toResponse(receipt))
            .build();
    }
}

Der Lernwert liegt im Container-Verständnis: CDI-Lifecycle, JTA-Grenzen, Security Context, Connection Pools, WAR-Deployment und Unterschiede zwischen fachlicher Transaktion und technischer Container-Transaktion.

Quarkus Lab

Quarkus ist interessant, wenn Container-Nähe, schnelle Startzeiten, geringerer Speicherbedarf und Native-Image-Optionen wichtig werden. Fachlich darf das aber nicht bedeuten, dass der Core Quarkus-spezifisch wird.

java
@Path("/orders")
public class OrderResource {
    @Inject PlaceOrderUseCase placeOrderUseCase;

    @POST
    public Response place(OrderRequest request) {
        var receipt = placeOrderUseCase.place(toCommand(request));
        return Response.status(201).entity(toResponse(receipt)).build();
    }
}

Das Quarkus-Projekt im Paket zeigt CDI Producer als Composition Root. Dadurch bleibt der Core sauber und Quarkus wird nur an der Grenze sichtbar.

Micronaut Lab

Micronaut legt den Schwerpunkt auf Compile-Time-DI, wenig Reflection und schnelle Starts. Das ist hilfreich für kleinere Services, Serverless-ähnliche Szenarien oder containerisierte Dienste mit vielen Instanzen.

java
@Controller("/orders")
public class OrderController {
    private final PlaceOrderUseCase placeOrderUseCase;

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

    @Post
    public HttpResponse<OrderResponse> place(@Body OrderRequest request) {
        var receipt = placeOrderUseCase.place(toCommand(request));
        return HttpResponse.created(toResponse(receipt));
    }
}

Der zentrale Unterschied liegt nicht im fachlichen Code, sondern in DI, HTTP-Stack, Build-Plugins, Runtime-Metadaten und Testunterstützung.

Portabilitäts-Landkarte

Portabilitäts-Landkarte

Codebestandteil Portabel Grundregel
Records für Commands Ja keine Framework-Annotation nötig
Domain Events Ja nur fachliche Fakten ausdrücken
Controller/Resource Nein bewusst dünn halten
Bean-Konfiguration Nein Composition Root im Adapterprojekt
Transaktionsannotation Teilweise Use Case fachlich schneiden, technische TX am Adapter setzen
Security Context Nein in fachliche User-/Tenant-Objekte übersetzen
Persistence Entity Teilweise nicht automatisch mit Domain Aggregate verwechseln

Teststrategie für alle vier Frameworks

Testmatrix für vier Framework-Labs

Die Teststrategie sollte nicht viermal dieselbe fachliche Logik testen. Besser:

  1. Core Unit Tests prüfen Businessregeln.
  2. Adapter Tests prüfen Mapping, HTTP-Status, Validation und Fehlerformat.
  3. Integration Tests prüfen Runtime-Wiring.
  4. Contract Tests sichern API-Verträge.
  5. Architekturtests prüfen, dass der Core keine Framework-Pakete importiert.
java
// Beispielregel für einen Architekturtest als Denkmodell:
// classes in ..core.. should not depend on org.springframework.., jakarta.ws.rs..,
// io.quarkus.. or io.micronaut..

Deployment-Formen

Deployment-Formen im Enterprise Java Alltag

Framework Typische Form Enterprise-Frage
Spring Boot ausführbares JAR / Container Image Wie standardisieren wir Actuator, Config, Security, Observability?
Jakarta EE WAR auf Application Server Wer betreibt den Container, Pools, JTA und Security Realms?
Quarkus Runner Jar / Native Image / Container Lohnt Native für diesen Dienst wirklich?
Micronaut ausführbares JAR / Container Profitieren wir von Compile-Time-DI und geringem Runtime-Footprint?

Maven- und Build-Regeln

Die Labs verwenden einen Parent-POM. Wichtig ist die Trennung zwischen Lehrbuch-Stabilität und Produktionsrealität.

xml
<modules>
  <module>order-platform-core</module>
  <module>spring-boot-order-api</module>
  <module>jakarta-ee-order-api</module>
  <module>quarkus-order-api</module>
  <module>micronaut-order-api</module>
</modules>

Für echte Projekte sollte man zusätzlich definieren:

  • Dependency Management über BOMs
  • Enforcer-Regeln für Java-Version, Maven-Version und Dependency-Konvergenz
  • SBOM-Erzeugung
  • OWASP Dependency Check oder SCA-Werkzeug
  • Checkstyle/Spotless
  • Architekturtests
  • Container-Image-Build
  • separate Profile für lokale Entwicklung und CI

Komplexerer Enterprise-Flow

Der Core enthält jetzt nicht nur PlaceOrderUseCase, sondern auch OrderFulfillmentSaga.

java
public OrderReceipt fulfill(OrderId orderId) {
    var order = repository.findById(orderId)
        .orElseThrow(() -> new IllegalArgumentException("order not found: " + orderId));

    inventoryPort.reserve(order.lines());
    var authorization = paymentGateway.authorize(order.id(), order.total());
    order.authorizePayment(authorization);

    var invoiceNumber = invoicePort.createInvoice(order);
    order.createInvoice(invoiceNumber);
    order.fulfill();

    repository.save(order);
    order.pullEvents().forEach(outboxPort::append);
    return new OrderReceipt(order.id(), order.status(), order.total());
}

Dieses Beispiel ist bewusst nicht als verteilte ACID-Transaktion dargestellt. In echten Systemen wären Kompensationen, Idempotenz, Retry, Outbox, Dead Letter Queue und Monitoring Pflicht.

Was du aus diesem Teil mitnimmst

  • Frameworks unterscheiden sich stark in Runtime und Adaptercode.
  • Fachliche Modellierung muss davon unabhängig bleiben.
  • Maven-Modulgrenzen sind Architekturgrenzen.
  • Controller und Resources dürfen nicht zum neuen Monolithen werden.
  • Ein gemeinsamer Core macht Migration, Vergleich, Tests und Refactoring einfacher.
  • Das richtige Framework hängt von Betriebsmodell, Teamwissen, Plattform, Integrationen und Lebensdauer ab.
Spring Boot

Spring Boot im Enterprise-Java-Kontext

Spring Boot ist für viele Enterprise-Teams die pragmatische Standardplattform. Es reduziert Konfiguration, integriert sehr viele Libraries und ermöglicht schnelle Service-Entwicklung. Trotzdem ersetzt Spring Boot keine Architektur. Ohne klare Regeln entstehen schnell Controller mit Geschäftslogik, Services mit SQL-Details und Repositories mit fachlichen Entscheidungen.

Typischer Einsatz

Einsatz Erklärung
REST APIs schnelle Bereitstellung mit Spring MVC oder WebFlux
Datenzugriff Spring Data JPA, JDBC, Transaktionsmanagement
Security Spring Security, OAuth2 Resource Server
Messaging Kafka, RabbitMQ, JMS-Integration
Betrieb Actuator, Health Checks, Metrics

Spring Boot Request Flow mit klarer Trennung.

Spring Boot Request Flow mit klarer Trennung.

Gute Schichtenregel

text
Controller  -> DTO, HTTP, Statuscodes
Use Case    -> fachliche Orchestrierung
Domain      -> Regeln, Zustände, Invarianten
Ports       -> Schnittstellen zu externen Systemen
Adapter     -> JPA, Kafka, REST Clients

Beispiel: Controller bleibt dünn

java
// Pattern: Adapter Pattern - HTTP wird in einen internen Command übersetzt.
@PostMapping("/orders")
ResponseEntity<OrderResponse> place(@Valid @RequestBody PlaceOrderRequest request) {
    var result = placeOrderUseCase.place(request.toCommand());
    URI location = URI.create("/orders/" + result.orderId());
    return ResponseEntity.created(location)
            .body(OrderResponse.from(result));
}

Typische Fehler

  • @Transactional überall verteilen, ohne klare Transaktionsgrenze.
  • Entity-Klassen direkt als REST Response ausgeben.
  • Business-Entscheidungen in Controller oder Repository verstecken.
  • Spring-spezifische Annotationen in die Domäne ziehen.
  • Actuator aktivieren, aber keine fachlichen Health Checks definieren.

Empfehlung

Nutze Spring Boot als Infrastrukturplattform. Halte den fachlichen Kern möglichst unabhängig. Dann kann man später einzelne Adapter austauschen, Tests einfacher schreiben und Migrationen kontrollierter durchführen.

Jakarta EE

Jakarta EE im Enterprise-Java-Kontext

Jakarta EE ist ein Standard-Ökosystem für Enterprise-Java-Anwendungen. Es ist besonders relevant, wenn Organisationen Applikationsserver, langfristige Kompatibilität, standardisierte APIs und klassische Enterprise-Patterns verwenden.

Jakarta EE Container Flow.

Jakarta EE Container Flow.

Wichtige APIs

API Zweck
Jakarta REST / JAX-RS REST-Endpunkte
CDI Dependency Injection und Lebenszyklen
JPA Persistenz und EntityManager
JTA Transaktionen über Ressourcen hinweg
Bean Validation Validierung von Eingaben und Modellen
JMS Messaging in klassischen Enterprise-Systemen
Batch Batch-Verarbeitung

Beispiel: CDI Service mit Transaktion

java
@ApplicationScoped
public class PlaceOrderService {
    @Inject OrderRepository repository;
    @Inject PaymentGateway paymentGateway;
    @Inject EventOutbox outbox;

    @Transactional
    public PlaceOrderResult place(PlaceOrderCommand command) {
        Order order = Order.place(command.customerId(), command.lines());
        PaymentDecision decision = paymentGateway.authorize(order.total());
        order.applyPayment(decision);
        repository.save(order);
        outbox.store(order.pullDomainEvents());
        return PlaceOrderResult.from(order);
    }
}

Stärke

Jakarta EE ist stark, wenn ein Unternehmen Standards gegenüber Framework-spezifischen Konventionen bevorzugt. Besonders bei Legacy-Modernisierung ist Jakarta wichtig, weil viele ältere Java-EE-Anwendungen von hier aus schrittweise modernisiert werden können.

Risiko

Der Container kann zu viel Magie verstecken. Deshalb müssen Transaktionen, Scopes, Lazy Loading und Nebenwirkungen sehr bewusst dokumentiert werden.

Quarkus & Micronaut

Quarkus und Micronaut im Enterprise-Java-Kontext

Quarkus und Micronaut stehen für eine modernere, stärker Cloud-Native-orientierte Sicht auf Java. Beide wollen Startzeit, Speicherverbrauch und Build-Zeit-Verhalten verbessern. Sie sind besonders interessant für Container, Kubernetes, Serverless-ähnliche Workloads und Microservices.

Quarkus und Micronaut im Gesamtvergleich.

Quarkus und Micronaut im Gesamtvergleich.

Quarkus

Quarkus ist stark in Kubernetes- und Cloud-Native-Szenarien. Es integriert viele bekannte APIs und Extensions. Der Entwicklungsmodus ist sehr produktiv, und das Framework legt Wert auf schnelle Starts und geringe Laufzeitkosten.

java
@Path("/orders")
public class OrderResource {
    private final PlaceOrderUseCase useCase;

    public OrderResource(PlaceOrderUseCase useCase) {
        this.useCase = useCase;
    }

    @POST
    public Response place(PlaceOrderRequest request) {
        return Response.status(201).entity(useCase.place(request.toCommand())).build();
    }
}

Micronaut

Micronaut ist stark bei compile-time Dependency Injection, leichter Runtime und klarer Testbarkeit. Es eignet sich gut für kleine bis mittlere Services, API-Gateways, Integrationen und Cloud-Funktionen.

java
@Controller("/orders")
final class OrderController {
    private final PlaceOrderUseCase useCase;

    OrderController(PlaceOrderUseCase useCase) {
        this.useCase = useCase;
    }

    @Post
    HttpResponse<OrderResponse> place(@Body PlaceOrderRequest request) {
        return HttpResponse.created(OrderResponse.from(useCase.place(request.toCommand())));
    }
}

Auswahlregel

Wähle Quarkus, wenn Kubernetes-Nähe, schnelle Starts, Extensions und Java-Cloud-Native im Vordergrund stehen. Wähle Micronaut, wenn compile-time DI, geringer Overhead und sehr klare Service-Strukturen wichtiger sind.

Framework-Labs getrennte Projekte

Framework-Labs getrennte Projekte

Überblick

Dieses Themenkapitel erklärt die neue Version-3-Codebasis aus Sicht eines Lesers, der die Dateien später direkt im Editor öffnen möchte.

text
code/framework-labs/
├── order-platform-core              # fachlicher Kern, JDK-only, geprüft
├── spring-boot-order-api            # Spring Boot HTTP Adapter
├── jakarta-ee-order-api             # Jakarta EE WAR/JAX-RS Adapter
├── quarkus-order-api                # Quarkus REST/CDI Adapter
└── micronaut-order-api              # Micronaut HTTP/DI Adapter

Reihenfolge beim Lesen

  1. order-platform-core/docs/design-patterns.html
  2. order-platform-core/src/main/java/.../domain/Order.java
  3. order-platform-core/src/main/java/.../application/PlaceOrderUseCase.java
  4. order-platform-core/src/main/java/.../application/OrderFulfillmentSaga.java
  5. danach je Framework den Controller oder die Resource öffnen

Was bewusst gleich ist

Gleiches Element Grund
PlaceOrderCommand API-Adapter erzeugen denselben fachlichen Auftrag
PlaceOrderUseCase Fachlicher Ablauf bleibt stabil
OrderRepository Port Persistenz bleibt austauschbar
OutboxPort Event-Ausgabe bleibt Core-nah, Implementierung technisch
Money, Order, OrderLine Domainregeln werden nicht in Framework-Klassen kopiert

Was bewusst unterschiedlich ist

Framework Unterschied
Spring Boot @RestController, @Configuration, Spring DI
Jakarta EE @Path, CDI Producer, WAR-Paketierung
Quarkus JAX-RS/RESTEasy + Arc CDI, container-first
Micronaut @Controller, @Factory, Compile-Time-DI

Mini-UML

text
+--------------------+       +---------------------+
| OrderController    | ----> | PlaceOrderUseCase   |
| OrderResource      |       +---------------------+
+--------------------+                 |
          |                             v
          |                  +---------------------+
          |                  | OrderRepository     |
          |                  | OutboxPort          |
          |                  +---------------------+
          v                             |
+--------------------+                 v
| HTTP Runtime       |       +---------------------+
| Spring/Jakarta/... |       | Adapter Impl        |
+--------------------+       +---------------------+

Architekturregel

Der Core importiert keine Frameworkpakete. Das ist der wichtigste Check der Version 3.

text
Erlaubt im Core:
java.*, java.time.*, java.math.*, eigene Domain- und Application-Pakete

Nicht erlaubt im Core:
org.springframework.*, jakarta.ws.rs.*, io.quarkus.*, io.micronaut.*
Framework Entscheidungsmatrix

Framework Entscheidungsmatrix

Zweck

Diese Matrix ist keine absolute Bewertung. Sie hilft, im Enterprise-Kontext sauberer zu argumentieren.

Kriterium Spring Boot Jakarta EE Quarkus Micronaut
Breites Ökosystem sehr stark mittel wachsend wachsend
Klassischer App-Server-Betrieb möglich, aber nicht Kernmodell sehr stark eher nein eher nein
Container-first stark abhängig vom Server sehr stark stark
Native Image Fokus möglich nicht Kernmodell sehr stark stark
Compile-Time DI teilweise nein Build-Time Optimierung stark
Legacy Java EE Nähe mittel sehr stark mittel gering-mittel
Lernkurve im Enterprise-Team oft niedrig abhängig von Historie mittel mittel
Risiko von Framework-Vermischung im Core mittel-hoch mittel mittel mittel

Empfehlung nach Situation

Situation Empfehlung
Unternehmen nutzt bereits stark Spring Spring Boot, aber Core sauber halten
bestehende WAR/EAR-Landschaft Jakarta EE als Brücke prüfen
sehr viele kleine Container-Services Quarkus oder Micronaut prüfen
Native Image wichtig Quarkus zuerst evaluieren
Compile-Time-DI wichtig Micronaut stark prüfen
maximale Portabilität fachlichen Core frameworkfrei halten

Warnsignale

  • Framework wird gewählt, bevor fachliche Grenzen klar sind.
  • Controller enthalten Preislogik, Statuslogik oder Transaktionsentscheidungen.
  • Domain-Klassen importieren HTTP-, DI- oder Persistence-Frameworks ohne bewusste Entscheidung.
  • Tests starten immer den ganzen Container, obwohl Core-Tests reichen würden.
  • Migration wird als Dependency-Update statt als Architekturarbeit verstanden.
ModernisierungRuntime-, Framework- und Abhängigkeitsmodernisierung.
Anwendungen modernisieren

Runtime und Abhängigkeiten modernisieren

Dieser Teil ergänzt das Enterprise-Java-Lese- und Referenzbuch um eine aktuelle Runtime-Modernisierungsschicht. Der wichtigste Punkt: Das Buch bleibt fachlich bei Java 11, Java 17 und Java 21, aber die Framework-Welt bewegt sich weiter. Deshalb werden neue Baselines sauber getrennt dokumentiert.

Runtime Roadmap

Ziel der Erweiterung

Nach Grundlagen, Architektur und Framework-Vergleich beantwortet dieser Teil die Frage:

Wie modernisiere ich ein Enterprise-Java-System 2026, ohne meine Java-21-Lernlinie, meine Architekturgrenzen und meine Betriebsfähigkeit zu zerstören?

Dazu wurden ergänzt:

  • neue Runtime-Matrix für Spring Boot 4.x, Jakarta EE 11, Quarkus 3.33 LTS, Micronaut 4/5
  • Dependency-Governance mit BOMs, Maven Enforcer, SBOM und CVE-Checks
  • Entscheidungsbaum für Upgrade-Pfade
  • Java-21-Core-Stabilitätsregel
  • separates Runtime-Modernization-Lab mit Java-Code
  • Maven-POM-Beispiele für Profile und Versionstrennung
  • neue SVG/PNG-Diagramme
  • aktualisierte Checks, Indexe und Was-wurde-gemacht-Dateien

Quellenstand und Einordnung

Stand: 08.07.2026. Für die Runtime-Entscheidungen wurden bewusst offizielle Projektquellen verwendet:

  • Spring Boot System Requirements: Spring Boot 4.1.0 verlangt Java 17 oder höher und Spring Framework 7.0.8 oder höher.
  • Spring Boot 4 Migration Guide: Spring Boot 4 basiert auf Jakarta EE 11, Servlet 6.1 und entfernt nicht mehr passende Altpfade wie Undertow-Support in diesem Release.
  • Jakarta EE 11 Release-Seite und Release Plan: Jakarta EE 11 modernisiert die Plattform, enthält Jakarta Data und richtet sich auf Java 17/21 aus; die Platform-API nutzt --release 17.
  • Quarkus Releases und Quarkus 3.33 Blog: Quarkus 3.33 ist die aktuelle LTS-Spur; LTS-Releases erhalten kritische Fixes und Security-Patches für 12 Monate.
  • Micronaut 5.0 Release und Baseline-Hinweis: Micronaut 5 setzt Java 25 als Baseline. Deshalb wird Micronaut 5 im Java-11/17/21-Buch als Zukunftsspur und nicht als Haupt-Lab geführt.

Die zentrale Entscheidung

Thema Entscheidung für dieses Buch
Java-Hauptlinie Java 11, 17 und 21 bleiben Kern des Buchs
Java-21-Core bleibt frameworkfrei und stabil
Spring Boot 4.x als aktueller Spring-Modernisierungspfad dokumentiert
Jakarta EE 11 als aktuelle Enterprise-Standardplattform dokumentiert
Quarkus 3.33 LTS als produktionsnaher Container-/Cloud-Native-Pfad dokumentiert
Micronaut 4.x bleibt sinnvoll für Java-21-orientierte Labs
Micronaut 5.x wird als Java-25-Zukunftspfad getrennt dokumentiert

Warum diese Trennung wichtig ist

Ein Enterprise-Projekt scheitert selten daran, dass eine neue Version nicht kompiliert. Es scheitert eher daran, dass unklare Baselines, vermischte Framework-Abhängigkeiten, unvollständige Tests und fehlende Betriebsnachweise den Release unkontrollierbar machen.

Deshalb gilt für die Modernisierung:

text
Fachlicher Core      -> Java 21, stabil, ohne Framework-Imports
Framework-Adapter    -> dürfen je Runtime modernisiert werden
Build Governance     -> Maven Enforcer, BOMs, Dependency Locks, SBOM
Betriebsnachweis     -> Health, Metrics, Logs, Images, Rollback
Zukunftsbaselines    -> separat dokumentieren, nicht heimlich mischen

Architekturregel: Core bleibt ruhig, Adapter bewegen sich

Dependency Governance Pipeline

Der Core enthält Domänenmodell, Use Cases, Ports, fachliche Fehler und Tests. Der Core sollte nicht wissen, ob die Anwendung später als Spring Boot JAR, Jakarta WAR, Quarkus Container oder Micronaut Service läuft.

java
// Pattern: Hexagonal Architecture / Ports and Adapters
public final class UpgradePlanner {
    private final RuntimePolicyRepository policies;
    private final RiskClassifier riskClassifier;

    public UpgradePlan planFor(ApplicationInventory inventory) {
        RuntimePolicy policy = policies.findBestPolicyFor(inventory.targetRuntime());
        return UpgradePlan.builder(inventory.name())
                .baseline(policy.recommendedJavaBaseline())
                .risk(riskClassifier.classify(inventory, policy))
                .addStep("Core gegen Java 21 prüfen")
                .addStep("Adapter-Dependencies getrennt aktualisieren")
                .addStep("SBOM und Lizenzbericht erzeugen")
                .addStep("Rollback und Betriebsmetriken dokumentieren")
                .build();
    }
}

Upgrade-Entscheidungsbaum

Upgrade Decision Tree

Entscheidung 1: Bestehender Application Server

Wenn eine Organisation viele WAR-Deployments, zentrale Transaktionen, Application-Server-Betrieb und Jakarta-Standards nutzt, ist Jakarta EE 11 der natürlichere Modernisierungspfad. Der Fokus liegt dann auf API-Update, Server-Kompatibilität, Packaging und Integrationstests.

Entscheidung 2: Spring-Ökosystem

Wenn Actuator, Spring Security, Spring Data, Spring Batch, Spring Cloud oder bestehende Spring-Kompetenz wichtig sind, ist Spring Boot 4.x die passende Modernisierungsspur. Man muss aber Dependency-Management, entfernte Deprecations und Servlet-6.1-Baseline bewusst prüfen.

Entscheidung 3: Container First

Wenn Startzeit, Memory-Footprint, Kubernetes-Nähe und native Build-Optionen im Vordergrund stehen, ist Quarkus 3.33 LTS eine gute Spur. In Version 4 wird Quarkus als Java-21-kompatibler Produktionspfad dokumentiert.

Entscheidung 4: Micronaut 5 und Java 25

Micronaut 5 ist fachlich interessant, aber für dieses Buch eine getrennte Zukunftsspur, weil die Hauptlinie Java 11/17/21 ist.

Micronaut Baseline Split

Spring Boot 4.x und Jakarta EE 11 Stack

Spring Boot 4 Jakarta EE 11 Stack

Spring Boot 4.x ist nicht nur ein Versionssprung von Boot 3.x. Es ist auch ein Schritt auf eine neue Spring-Framework-Generation, eine Jakarta-EE-11-Grundlage und eine neue Servlet-Baseline. Daraus folgen typische Aufgaben:

  • veraltete APIs aus Boot 3.x vor dem Sprung entfernen
  • Starter-Abhängigkeiten prüfen
  • Security-Konfigurationen testen
  • Servlet-Container-Kompatibilität klären
  • Observability und Actuator-Endpunkte prüfen
  • Integrationstests gegen echte HTTP-Flows ausführen

Jakarta EE 11 im Buchkontext

Jakarta EE 11 ist wichtig, weil viele Enterprise-Landschaften nicht nur aus Microservices bestehen. Es gibt weiterhin Application Server, WARs, zentrale Betriebsmodelle, JTA, JPA, JMS, Security-Domänen und klassische Mandantenlogik.

Für das Buch bedeutet das:

text
Jakarta EE 11 ist nicht altmodisch.
Es ist die Standardplattform für containerverwaltete Enterprise-Java-Systeme.

Quarkus 3.33 LTS im Buchkontext

Quarkus 3.33 LTS wird als stabile Produktionsspur für containernahe Java-Services ergänzt. Die Lernregel bleibt:

text
Quarkus-spezifisch: Resource, CDI-Wiring, Config, Extension-Auswahl
Core-spezifisch: Domain, Use Case, Ports, fachliche Tests

Dependency-Governance statt Versionsraten

Eine moderne Enterprise-Java-Plattform braucht eine zentrale Steuerung:

xml
<properties>
  <java.release>21</java.release>
  <spring.boot.version>4.1.0</spring.boot.version>
  <jakarta.ee.version>11.0.0</jakarta.ee.version>
  <quarkus.platform.version>3.33.1.1</quarkus.platform.version>
  <micronaut.java21.track>4.x</micronaut.java21.track>
  <micronaut.java25.track>5.0.0</micronaut.java25.track>
</properties>

Wichtig: Diese Werte sind Lern- und Architekturanker. In einem echten Projekt müssen sie über Repository-Policy, Vendor-Support, CVE-Status, Build-Reproduzierbarkeit und Testabdeckung freigegeben werden.

Release-Train-Matrix

Release Train Matrix

Track Aufgabe Nachweis
Baseline Track Java 21 Core stabilisieren javac --release 21, Unit Tests
Framework Track Adapter je Runtime aktualisieren HTTP-/DI-/Config-Tests
Security Track Dependencies prüfen SBOM, CVE, Lizenzbericht
Ops Track Containerbetrieb absichern Health, Metrics, Logs, Rollback

Neues Runtime-Modernization-Lab

Version 4 ergänzt ein eigenes Lab:

text
code/runtime-modernization-lab/
├── pom.xml
├── README.md / README.html
├── docs/design-patterns.md / design-patterns.html
└── src/main/java/com/example/enterprisejava/runtime2026/

Das Lab ist bewusst JDK-only kompilierbar, damit es in dieser Umgebung ohne Maven-Download geprüft werden kann. Die POM-Datei enthält trotzdem die Enterprise-Modernisierungslogik mit zentralen Properties, Enforcer-Kommentaren und Profilen.

Lernziel dieses Teils

Danach soll man nicht nur wissen, welche Framework-Version modern ist. Man soll erklären können:

  • warum Java 21 als fachlicher Core stabil bleiben kann
  • warum Spring Boot 4.x und Jakarta EE 11 zusammen betrachtet werden müssen
  • warum Quarkus LTS für Produktion anders zu behandeln ist als normale Minor Releases
  • warum Micronaut 5 wegen Java 25 getrennt betrachtet wird
  • wie Dependency-Governance ein Upgrade kontrollierbar macht
  • welche Design Patterns helfen, Framework-Wechsel ohne Fachlogik-Umbau zu ermöglichen
Runtime-Modernisierung 2026

Runtime-Modernisierung 2026: stabile Java-21-Lernlinie mit aktuellen Frameworks

Dieses Kapitel ist die operative Anleitung zu Version 4. Es beantwortet: Wie gehe ich in einem echten Enterprise-Projekt vor, wenn die Framework-Welt aktueller ist als die Java-LTS-Zielarchitektur des Unternehmens?

Quellenstand und Einordnung

Stand: 08.07.2026. Für die Runtime-Entscheidungen wurden bewusst offizielle Projektquellen verwendet:

  • Spring Boot System Requirements: Spring Boot 4.1.0 verlangt Java 17 oder höher und Spring Framework 7.0.8 oder höher.
  • Spring Boot 4 Migration Guide: Spring Boot 4 basiert auf Jakarta EE 11, Servlet 6.1 und entfernt nicht mehr passende Altpfade wie Undertow-Support in diesem Release.
  • Jakarta EE 11 Release-Seite und Release Plan: Jakarta EE 11 modernisiert die Plattform, enthält Jakarta Data und richtet sich auf Java 17/21 aus; die Platform-API nutzt --release 17.
  • Quarkus Releases und Quarkus 3.33 Blog: Quarkus 3.33 ist die aktuelle LTS-Spur; LTS-Releases erhalten kritische Fixes und Security-Patches für 12 Monate.
  • Micronaut 5.0 Release und Baseline-Hinweis: Micronaut 5 setzt Java 25 als Baseline. Deshalb wird Micronaut 5 im Java-11/17/21-Buch als Zukunftsspur und nicht als Haupt-Lab geführt.

Modernisierung ist mehrdimensional

Viele Teams behandeln Modernisierung wie eine Liste neuer Versionen. Das ist gefährlich. In Enterprise-Systemen müssen mindestens sechs Dimensionen gleichzeitig betrachtet werden:

Dimension Leitfrage Typisches Risiko
Sprache Welche Java-Version ist erlaubt? falsche Baseline, Build bricht
Framework Welche Runtime wird getragen? inkompatible Starter oder APIs
Build Sind BOMs und Plugins konsistent? transitive Konflikte
Security Gibt es CVEs oder Lizenzrisiken? ungeprüfte Produktion
Betrieb Kann es deployed, beobachtet und zurückgerollt werden? Ausfall ohne Diagnose
Team Kann das Team die Runtime warten? Bus-Faktor, Know-how-Lücke

Die wichtigste Buchregel

text
Der Java-21-Core ist kein Spielball der Framework-Versionen.
Framework-Adapter dürfen sich ändern, aber die Fachlogik bleibt stabil.

Modernisierungssequenz

  1. Anwendung inventarisieren: Java-Version, Framework, Packaging, Datenbank, Messaging, Security.
  2. Fachlichen Core identifizieren und Framework-Imports entfernen.
  3. Java-21-Kompilierung mit --release 21 erzwingen.
  4. Runtime-Ziel wählen: Spring Boot 4.x, Jakarta EE 11, Quarkus 3.33 LTS, Micronaut 4.x oder Java-25-Spur.
  5. BOMs zentralisieren.
  6. Maven Enforcer Regeln aktivieren.
  7. SBOM und Lizenzbericht erzeugen.
  8. Integrationstests gegen echte Adapter laufen lassen.
  9. Betriebsnachweise prüfen: Health, Metrics, Logs, Rollback.
  10. Ergebnis im Architekturentscheid dokumentieren.

Beispiel: ADR für Runtime-Entscheidung

markdown
# ADR: Runtime-Modernisierung Order API 2026

## Entscheidung
Wir halten den fachlichen Core auf Java 21 stabil und modernisieren den HTTP-Adapter auf Spring Boot 4.x.

## Begründung
Das Team nutzt Spring Security, Actuator und Spring Data intensiv. Der Core enthält keine Spring-Imports.

## Konsequenzen
- Spring Framework 7.x und Jakarta EE 11 Baseline prüfen
- Servlet-6.1-Kompatibilität sicherstellen
- Integrationstests und Security-Tests aktualisieren
- SBOM und Rollback-Dokument erzeugen

Typische Fehler

Fehler Folge Bessere Vorgehensweise
Framework-Update im Core Wechsel wird teuer Ports & Adapters erzwingen
Versionen direkt in Modulen Drift und Konflikte zentrale Properties / BOMs
nur Unit Tests Adapterfehler bleiben unsichtbar HTTP- und DI-Tests ergänzen
kein SBOM Security nicht nachweisbar CycloneDX/Dependency-Check ergänzen
Micronaut 5 heimlich in Java-21-Projekt Baseline-Konflikt Java-25-Spur separat führen
Spring Boot 4 & Jakarta EE 11

Spring Boot 4.x und Jakarta EE 11: Modernisierung ohne Architekturbruch

Spring Boot 4.x und Jakarta EE 11 gehören in der Modernisierung zusammen betrachtet, weil Spring Boot 4 auf einer neuen Jakarta-EE-Grundlage und Servlet-6.1-Baseline sitzt. Das bedeutet: Auch wenn die Anwendung wie eine normale Spring-Boot-Anwendung aussieht, ändern sich darunter API- und Containerannahmen.

Spring/Jakarta Stack

Was sich fachlich nicht ändern sollte

Die folgenden Klassen sollten nicht wissen, ob Spring Boot 4 oder Jakarta EE 11 verwendet wird:

text
Order
OrderLine
Money
PlaceOrderUseCase
PaymentPort
OutboxPort
OrderRepository

Diese Klassen gehören in den Core.

Was sich ändern darf

text
Spring @RestController
Spring @Configuration
Spring Security Konfiguration
Jakarta @Path Resource
CDI Producer
Exception Mapper
Servlet Packaging
Actuator / Health Endpoints

Diese Klassen gehören in Adapter.

Upgrade-Checkliste Spring Boot 4.x

Schritt Prüfung
Java mindestens Java 17, im Buch Java 21 bevorzugt
Spring Framework 7.x Linie prüfen
Jakarta Jakarta EE 11 Baseline beachten
Servlet Servlet 6.1 Container prüfen
Dependencies Starter statt Einzelabhängigkeiten bevorzugen
Security Tests für AuthN/AuthZ aktualisieren
Observability Actuator/Metrics/Tracing prüfen
Rollback Boot-3.x- und Boot-4.x-Artefakte trennbar halten

Beispiel: Core bleibt frei

java
// Kein Spring, kein Jakarta, kein Quarkus, kein Micronaut im Core.
public interface PaymentPort {
    PaymentDecision authorize(PaymentRequest request);
}

Beispiel: Spring Adapter

java
// Pattern: Adapter - HTTP und Framework-Anmerkungen bleiben außen.
@RestController
@RequestMapping("/orders")
final class OrderController {
    private final PlaceOrderUseCase useCase;

    OrderController(PlaceOrderUseCase useCase) {
        this.useCase = useCase;
    }
}

Beispiel: Jakarta Adapter

java
// Pattern: Adapter - JAX-RS Resource übersetzt HTTP in Use Case.
@Path("/orders")
public class OrderResource {
    @Inject PlaceOrderUseCase useCase;
}

Entscheidungsregel

Wenn das Unternehmen bereits Spring-Kompetenz und Spring-Ökosystem verwendet, ist Spring Boot 4.x naheliegend. Wenn es stark auf standardisierte Application Server, WARs und containerverwaltete Enterprise Services setzt, ist Jakarta EE 11 naheliegender.

Quarkus LTS & Micronaut-Entscheidung 2026

Quarkus 3.33 LTS und Micronaut-Entscheidung 2026

Quarkus und Micronaut sind beide moderne Enterprise-Java-Frameworks, aber in Version 4 werden sie unterschiedlich behandelt.

Micronaut Baseline Split

Quarkus 3.33 LTS

Quarkus 3.33 LTS wird im Buch als produktionsnahe Container- und Cloud-Native-Spur geführt. Die LTS-Eigenschaft ist wichtig, weil Enterprise-Projekte nicht jede Minor-Version sofort übernehmen können.

Gute Einsatzfälle

Fall Warum Quarkus passt
Kubernetes-first Quarkus ist stark auf Containerbetrieb optimiert
schnelle Starts wichtig bei Skalierung und Rollouts
kleine Services geringe Runtime-Last ist vorteilhaft
native Images optionaler Pfad für spezielle Anforderungen

Micronaut 4.x für Java-21-Labs

Micronaut bleibt im Java-21-Lernbuch sinnvoll, solange die verwendete Spur Java 21 unterstützt. In Version 4 wird Micronaut deshalb als Java-21-kompatibler Track und zusätzlich als Java-25-Zukunftspfad dokumentiert.

Micronaut 5.x als Java-25-Zukunftspfad

Micronaut 5 setzt Java 25 als Baseline. Das ist kein Nachteil, aber es passt nicht direkt in ein Buch, dessen Hauptziel Java 11, 17 und 21 ist. Deshalb gilt:

text
Micronaut 4.x -> Java-21-Lab
Micronaut 5.x -> Java-25-Zukunftskapitel

Entscheidungsmatrix

Kriterium Quarkus 3.33 LTS Micronaut 4.x Micronaut 5.x
Java-21-Buchlinie sehr gut passend passend nicht direkt passend
Container-Startzeit sehr stark stark stark
LTS-Spur 3.33 LTS abhängig vom Projektplan Zukunftsspur
Java-Baseline Java-21-fähig im Buchkontext Java-21-fähig im Buchkontext Java 25
Empfehlung im Paket Produktionsspur Vergleichslab separates Zukunftskapitel

Musterentscheidung

text
Für ein Java-21-Enterprise-Lernprojekt nehmen wir Quarkus 3.33 LTS und Micronaut 4.x.
Micronaut 5 wird als Java-25-Track vorbereitet, aber nicht in den Java-21-Core integriert.
Dependency Upgrade Matrix 2026

Dependency-Upgrade-Matrix 2026

Diese Referenz ist die kompakte Matrix für Version 4. Sie dient als Entscheidungs- und Prüfblatt für Enterprise-Java-Upgrades.

Hauptmatrix

Komponente 2026-Spur im Paket Java-Baseline im Buch Hinweis
Java Core Java 21 Java 21 fachlicher Kern bleibt stabil
Spring Boot 4.x / 4.1.0 als Referenz Java 17+, im Buch Java 21 basiert auf Spring Framework 7.x und Jakarta EE 11
Jakarta EE 11 Java 17 API, Java 21 nutzbar Standardplattform für Enterprise-Container
Quarkus 3.33 LTS Java 21 im Buchkontext LTS-Spur für Produktionsnähe
Micronaut 4.x für Java-21-Lab Java 21 Vergleichslab
Micronaut 5.x Java 25 getrennte Zukunftsspur

Build-Regeln

Regel Maven-Baustein Zweck
Java-Version erzwingen maven-compiler-plugin + maven-enforcer-plugin keine falsche Baseline
Abhängigkeiten zentralisieren BOM / Dependency Management keine Versionsdrift
doppelte Klassen vermeiden Enforcer Ban Duplicate Classes weniger Runtime-Konflikte
CVE prüfen OWASP Dependency Check Security-Nachweis
SBOM erzeugen CycloneDX Maven Plugin Lieferketten-Inventar
Lizenz prüfen License Plugin Copyright-/Lizenz-Nachweis

Beispiel-POM-Ausschnitt

xml
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>${spring.boot.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

Freigaberegel

Eine Dependency wird nicht freigegeben, weil sie neu ist. Sie wird freigegeben, wenn folgende Nachweise vorhanden sind:

text
Build OK
Tests OK
SBOM vorhanden
CVE-Risiko bewertet
Lizenzstatus bewertet
Betriebsprobe bestanden
Rollback dokumentiert
Java Runtime Support Matrix 2026

Java Runtime Support Matrix 2026

Diese Matrix trennt Lernlinie, Produktionslinie und Zukunftslinie.

Linie Java Zweck Empfehlung
Legacy-Verständnis 11 alte Enterprise-Systeme verstehen nur noch bewusst verwenden
Migrationsbasis 17 moderne Mindestbasis vieler Frameworks guter Zwischenschritt
Buch-Hauptlinie 21 LTS, Virtual Threads, moderne Sprache Hauptziel dieses Pakets
Zukunftspfad 25 neue Framework-Baselines wie Micronaut 5 separat planen

Java 11

Java 11 ist im Buch wichtig, weil viele reale Enterprise-Systeme von Java 8 auf Java 11 modernisiert wurden oder noch auf Java 11 stehen. Für neue Architekturentscheidungen ist Java 11 aber meist nicht mehr die Zielbasis.

Java 17

Java 17 ist eine wichtige Mindestbasis, weil viele moderne Frameworks mindestens Java 17 verlangen. Wer von Java 8/11 kommt, kann Java 17 als technische Brücke nutzen.

Java 21

Java 21 ist die Hauptlinie des Pakets. Sie ist stark genug für moderne Sprache, Virtual Threads und stabile Enterprise-Architekturen.

Java 25

Java 25 wird nicht ignoriert, aber getrennt behandelt. Sobald ein Framework Java 25 als Baseline setzt, entsteht eine eigene Modernisierungsspur mit Build-, Runtime- und Betriebsfolgen.

NachschlagenAnnotationen, Maven-Plugins und Glossar.
Annotationen Enterprise Java

Große Annotationen-Sammlung Enterprise Java

Diese Sammlung erklärt häufige Annotationen aus Java, Jakarta, JPA, Bean Validation, Spring und Testing. Sie ist als Nachschlagewerk gedacht.

Java und JVM-nahe Annotationen

Annotation Bereich Zweck
@Override Java Methode überschreibt Supertype-Methode
@Deprecated Java API soll nicht mehr verwendet werden
@SuppressWarnings Java gezielte Compiler-Warnungen unterdrücken
@FunctionalInterface Java Interface soll genau eine abstrakte Methode haben

Jakarta / CDI

Annotation Zweck
@ApplicationScoped eine Instanz für die Anwendung
@RequestScoped Lebenszyklus pro Request
@Inject Dependency Injection
@Produces Factory/Producer für Beans
@Transactional Transaktionsgrenze

JPA

Annotation Zweck
@Entity persistente Klasse
@Table Tabellenzuordnung
@Id Primärschlüssel
@GeneratedValue Schlüsselgenerierung
@Column Spaltenmapping
@Version Optimistic Locking
@OneToMany Beziehung 1:n
@ManyToOne Beziehung n:1
@Embeddable Value Object in Entity einbetten

Bean Validation

Annotation Zweck
@NotNull Wert muss vorhanden sein
@NotBlank String darf nicht leer sein
@Size Länge oder Anzahl begrenzen
@Min / @Max numerische Grenze
@Pattern Regex-Regel
@Valid verschachtelte Validierung auslösen

Spring

Annotation Zweck
@SpringBootApplication Bootstrapping
@RestController REST Controller
@Service Service Bean
@Repository Repository Bean und Exception Translation
@Configuration Konfigurationsklasse
@Bean Bean Factory Method
@Transactional Transaktionsgrenze
@ControllerAdvice globale Fehlerbehandlung
@ExceptionHandler Fehler auf HTTP Response abbilden

Testing

Annotation Zweck
@Test Testmethode
@BeforeEach Setup vor jedem Test
@ParameterizedTest parametrisierter Test
@SpringBootTest Spring Integration Test
@DataJpaTest JPA Slice Test
@Testcontainers Containerbasierte Integration Tests
Maven Plugin Sammlung

Maven Plugin Sammlung für Enterprise Java

Diese Sammlung nennt wichtige Maven Plugins mit typischem Zweck und Beispielkonfiguration.

Compiler Plugin

xml
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>3.13.0</version>
  <configuration>
    <release>21</release>
  </configuration>
</plugin>

Enforcer Plugin

xml
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-enforcer-plugin</artifactId>
  <version>3.5.0</version>
  <executions>
    <execution>
      <goals><goal>enforce</goal></goals>
      <configuration>
        <rules>
          <requireJavaVersion><version>[21,)</version></requireJavaVersion>
          <requireMavenVersion><version>[3.9,)</version></requireMavenVersion>
        </rules>
      </configuration>
    </execution>
  </executions>
</plugin>

Surefire und Failsafe

xml
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>3.3.1</version>
</plugin>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-failsafe-plugin</artifactId>
  <version>3.3.1</version>
</plugin>

JaCoCo

xml
<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.12</version>
  <executions>
    <execution><goals><goal>prepare-agent</goal></goals></execution>
    <execution><id>report</id><phase>verify</phase><goals><goal>report</goal></goals></execution>
  </executions>
</plugin>

Versions Plugin

xml
<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>versions-maven-plugin</artifactId>
  <version>2.17.1</version>
</plugin>

Regel

Plugin-Versionen sollten niemals implizit bleiben. Ein reproduzierbarer Enterprise-Build kontrolliert jedes Plugin bewusst.

Glossar

Glossar Enterprise Java

Begriff Erklärung
Aggregate fachliche Konsistenzgrenze im Domain-Driven Design
Adapter technische Implementierung eines Ports, zum Beispiel JPA oder HTTP
Application Service orchestriert einen Use Case, enthält aber möglichst wenig Detailtechnik
DTO Data Transfer Object für API- oder Integrationsgrenzen
Entity Objekt mit Identität und Lebenszyklus
Idempotenz wiederholte Ausführung führt nicht zu mehrfacher fachlicher Wirkung
Outbox Tabelle für noch zu veröffentlichende Events
Port fachliche Schnittstelle, die vom Kern definiert wird
Repository fachliche Sicht auf Persistenz
Transaction Boundary Grenze, innerhalb der Daten konsistent geändert werden
Value Object unveränderlicher Wert ohne eigene Identität
Virtual Thread leichtgewichtiger Thread in Java 21 für blockierende Aufgaben
Code-DokumentationProjekte, Architekturhinweise, Diagramme und wichtige Quelldateien.

Code lesen, ohne sich in Dateien zu verlieren

Die Beispiele zeigen denselben Architekturgedanken in unterschiedlichen Größen. Beginne nicht alphabetisch bei Packages, sondern beim technischen Einstieg und folge genau einem fachlichen Ablauf bis zu den ausgehenden Ports.

Landkarte vom Einstieg über Adapter, Use Case und Domäne zu den technischen Ports

Welches Projekt beantwortet welche Frage?

ProjektLeitfrageEinstieg
enterprise-java-platform-labWie werden Domäne, Use Case, Ports und Adapter ohne Framework sichtbar?platform/support/DemoApplication.java
enterprise-order-billing-demoWie läuft eine Bestellung durch Payment, Billing und Outbox?orderbilling/DemoApplication.java
order-platform-coreWelche Teile bleiben bei einem Frameworkwechsel stabil?core/support/DemoApplication.java
Framework-AdapterWie wird HTTP in denselben Use Case übersetzt?OrderController oder OrderResource
runtime-modernization-labWie entsteht aus Inventar, Zielruntime und Policy ein Upgradeplan?runtime2026/DemoApplication.java

Empfohlene Leserichtung

  1. Bei DemoApplication, OrderController oder OrderResource beginnen.
  2. Den aufgerufenen Use Case und nur dessen Orchestrierung lesen.
  3. Domänenmethoden mit Invarianten und Zustandswechseln verfolgen.
  4. Die verwendeten Port-Interfaces notieren.
  5. Zu jedem Port genau einen Adapter ansehen.
  6. Zum Schluss Events und Outbox prüfen.

Eine Änderung sicher durchführen

Neue fachliche Regeln beginnen im Core. Externe Fähigkeiten erhalten einen kleinen Port. Danach folgen Use Case, Fake-Adapter und Test. Erst zuletzt werden Frameworkverdrahtung, HTTP-Fehlervertrag, Telemetrie und produktive Adapter angepasst. So bleibt ein fachlicher Umbau von Spring-, Jakarta-, Quarkus- oder Micronaut-Konfiguration getrennt.

Grenzen der Beispiele

In-Memory-Adapter, Mock-Payment und Console-Outbox erklären Schnittstellen und Ablauf. Für Produktion müssen Transaktionsgrenzen, Idempotenz, Timeouts, Wiederholungen, Security, Telemetrie und Schemaevolution ergänzt werden.

Vollständige Code-Leseführung als Markdown öffnen →

Platform Lab
enterprise-java-platform-lab – Überblick

Enterprise Java Platform Lab

Dieses Maven/JDK-Lab zeigt Enterprise-Java-Architektur ohne externe Framework-Abhängigkeiten. Dadurch sind Domäne, Ports, Adapter und Use Cases klar sichtbar.

Start

bash
javac --release 21 $(find src/main/java -name "*.java")
java -cp src/main/java com.example.enterprisejava.platform.support.DemoApplication

Inhalt

  • Domain Model mit Value Objects und Aggregate Root
  • Application Service / Use Case
  • Repository, Payment und Outbox Ports
  • Adapter-Implementierungen
  • Refactoring-Beispiel vorher/nachher
  • Java-21-Beispiel mit Virtual Threads
  • Design-Pattern-Dokumentation unter docs/design-patterns.md
enterprise-java-platform-lab – Entwurfsmuster

Design Patterns im Enterprise Java Platform Lab

Pattern Zweck Einsatzort Begründung
Value Object Fachliche Werte unveränderlich modellieren Money, OrderId, CustomerId, OrderLine verhindert primitive obsession und ungültige Zustände
Aggregate Root Invarianten und Zustandswechsel schützen Order zentrale fachliche Regeln und Domain Events
Domain Event fachliche Tatsachen ausdrücken DomainEvent, OrderPlacedEvent lose Kopplung zwischen Domäne und Integration
Result Pattern fachliche Alternativen explizit modellieren PaymentDecision keine null-Werte oder Exceptions für normale Fachfälle
Repository Persistenz abstrahieren OrderRepository, InMemoryOrderRepository Anwendung hängt nicht an JPA/JDBC
Port and Adapter externe Systeme entkoppeln PaymentPort, OutboxPort, Adapter testbar und frameworksensibel
Strategy austauschbare Preisregeln PricingPolicy, DefaultPricingPolicy Preislogik kann je Kunde/Land/Kanal variieren
Application Service / Use Case Ablauf orchestrieren PlaceOrderUseCase klare Transaktions- und Anwendungsgrenze
Facade Zugriff vereinfachen OrderQueryFacade, RestOrderControllerSimulator Adapter erhalten einfache Schnittstellen
Outbox Events zuverlässig publizieren OutboxPort, ConsoleOutboxPort echte Systeme ersetzen Console durch Outbox-Tabelle
Correlation Context Nachvollziehbarkeit Dokumentiert in Observability-Kapitel verteilte Fehleranalyse
Order & Billing Demo
enterprise-order-billing-demo / docs – Überblick

Enterprise Order Billing Demo

Kleines Maven-Beispielprojekt für das Buch. Es ist bewusst frameworkarm gehalten, damit die Architekturprinzipien sichtbar bleiben.

Enthaltene Konzepte

  • Aggregate Root
  • Value Object
  • Repository Pattern
  • Port/Adapter
  • Facade/Application Service
  • Outbox Pattern
  • Strategy Pattern
  • Result Pattern mit sealed interfaces

Start

bash
mvn -q package
java -cp target/enterprise-order-billing-demo-1.0.0.jar com.example.enterprisejava.orderbilling.DemoApplication
enterprise-order-billing-demo – Entwurfsmuster

Design Patterns im Enterprise-Java-Paket

Diese Datei dokumentiert die verwendeten Entwurfsmuster. Im Maven-Beispielprojekt sind die wichtigsten Pattern zusätzlich direkt im Code kommentiert.

Pattern Zweck Einsatzort Begründung
Repository Pattern Persistenz fachlich kapseln OrderRepository Domain/Application soll keinen EntityManager kennen
Adapter Pattern externe Systeme kapseln PaymentGatewayAdapter Payment-API kann gewechselt werden
Facade Pattern Use Case nach außen einfach halten PlaceOrderUseCase REST muss keine Fachschritte orchestrieren
Factory Pattern komplexe Erzeugung zentralisieren OrderFactory Validierung und Konstruktion bleiben konsistent
Strategy Pattern austauschbare Preislogik PricingStrategy Rabattregeln variieren nach Kundentyp
Outbox Pattern Events zuverlässig veröffentlichen OutboxMessageRepository Datenänderung und Event-Absicht in einer Transaktion
Result Pattern fachliche Alternativen typisiert ausdrücken PaymentDecision Keine null-Rückgaben oder technische Exceptions für erwartete Fälle
Idempotent Consumer doppelte Events sicher behandeln BillingEventHandler Messaging liefert praktisch mindestens einmal
Mapper Pattern API und Domain trennen OrderApiMapper DTOs und Domainmodell entwickeln sich unabhängig
Specification Pattern komplexe Regeln kombinieren OrderSpecification Regeln werden testbar und benennbar

Beispiel: Strategy Pattern

java
public interface PricingStrategy {
    Money calculate(Order order);
}

public final class EnterpriseCustomerPricing implements PricingStrategy {
    public Money calculate(Order order) {
        return order.rawTotal().minusPercent(10);
    }
}

Bezug zum Maven-Projekt

Die Code-Kommentare markieren die verwendeten Patterns direkt an Klassen und Methoden. Dadurch kann man beim Lesen zwischen Theorie, HTML-Referenz und Java-Dateien wechseln.

Framework-Labs
Allgemein
framework-labs – Überblick

Framework Labs: Spring Boot, Jakarta EE, Quarkus und Micronaut

Dieses Verzeichnis ergänzt das Enterprise-Java-Lese- und Referenzbuch um Version 3.

Struktur

text
framework-labs/
├── pom.xml
├── order-platform-core/
├── spring-boot-order-api/
├── jakarta-ee-order-api/
├── quarkus-order-api/
└── micronaut-order-api/

Lernziel

Der gleiche fachliche Kern wird von vier technischen Adapterprojekten verwendet. Dadurch sieht man klar, was wirklich Framework-spezifisch ist und was im Enterprise-Projekt besser frameworkfrei bleibt.

Geprüft

Der JDK-only Core wurde mit javac --release 21 geprüft. Die Framework-Projekte sind Referenz-Labs mit Maven-POMs und Codebeispielen; zum vollständigen Build werden externe Dependencies benötigt.

Jakarta EE
framework-labs / jakarta-ee-order-api – Überblick

Jakarta EE Order API

Dieses Projekt zeigt denselben fachlichen Use Case wie order-platform-core, aber mit Jakarta EE-typischem Adapter-Stil.

Wichtig

Der fachliche Code bleibt im Core. Dieses Projekt enthält nur:

  • HTTP-Schicht
  • Dependency Injection / Wiring
  • Fehlerbehandlung
  • Security- oder Runtime-Hinweise
  • Framework-spezifische Konfiguration

Lernhinweis

Jakarta EE passt besonders bei Application-Server-Betrieb, zentral gemanagten Transaktionen, WAR-Deployments und bestehenden Java-EE-Landschaften.

Typische Startbefehle

bash
mvn -pl code/framework-labs/jakarta-ee-order-api -am test
mvn -pl code/framework-labs/jakarta-ee-order-api -am package

Ohne Internetzugriff werden Framework-Abhängigkeiten nicht heruntergeladen. Der JDK-only Core ist im Paket lokal mit javac --release 21 geprüft.

framework-labs / jakarta-ee-order-api – Entwurfsmuster

Design Patterns in Jakarta EE Order API

Pattern Zweck Einsatzort Begründung
Resource Adapter HTTP nach Use Case übersetzen OrderResource JAX-RS bleibt Adapter
CDI Producer Framework-Wiring isolieren CoreProducer Core bleibt unabhängig vom Container
Exception Mapper fachliche Fehler mappen BusinessExceptionMapper klare API-Fehler im WAR
Micronaut
framework-labs / micronaut-order-api – Überblick

Micronaut Order API

Dieses Projekt zeigt denselben fachlichen Use Case wie order-platform-core, aber mit Micronaut-typischem Adapter-Stil.

Wichtig

Der fachliche Code bleibt im Core. Dieses Projekt enthält nur:

  • HTTP-Schicht
  • Dependency Injection / Wiring
  • Fehlerbehandlung
  • Security- oder Runtime-Hinweise
  • Framework-spezifische Konfiguration

Lernhinweis

Micronaut eignet sich gut, wenn Compile-Time-DI, geringer Reflection-Anteil, schnelle Starts und kompakter Runtime-Footprint im Vordergrund stehen.

Typische Startbefehle

bash
mvn -pl code/framework-labs/micronaut-order-api -am test
mvn -pl code/framework-labs/micronaut-order-api -am package

Ohne Internetzugriff werden Framework-Abhängigkeiten nicht heruntergeladen. Der JDK-only Core ist im Paket lokal mit javac --release 21 geprüft.

framework-labs / micronaut-order-api – Entwurfsmuster

Design Patterns in Micronaut Order API

Pattern Zweck Einsatzort Begründung
Thin Controller HTTP von Use Case trennen OrderController Controller bleibt Adapter
Factory / Composition Root Beans zentral erzeugen CoreFactory Core hat keine Micronaut-Abhängigkeit
Ports and Adapters Core portabel halten Core-Ports Framework kann gewechselt werden
Gemeinsamer Core
framework-labs / order-platform-core – Überblick

Order Platform Core

Dieses Modul ist der fachliche Kern der Version-3-Framework-Labs.

Ziel

Der Core enthält bewusst keine Spring-, Jakarta-, Quarkus- oder Micronaut-Abhängigkeit. Dadurch kann dieselbe Domäne in mehreren Framework-Projekten verwendet werden.

Enthalten

  • Domain Model: Order, OrderLine, Money, OrderId
  • Domain Events: OrderPlacedEvent, PaymentAuthorizedEvent, InvoiceCreatedEvent
  • Application Services: PlaceOrderUseCase, OrderFulfillmentSaga
  • Ports: OrderRepository, PaymentGateway, InventoryPort, InvoicePort, OutboxPort
  • Demo-Adapter: In-Memory Repository, Mock Payment, Mock Inventory, Mock Invoice, Console Outbox

Lokaler Compile-Check

bash
javac --release 21 $(find src/main/java -name "*.java")
framework-labs / order-platform-core – Entwurfsmuster

Design Patterns im Order Platform Core

Pattern Zweck Einsatzort Begründung
Value Object Primitive Werte fachlich kapseln OrderId, CustomerId, Money, OrderLine verhindert primitive obsession und macht Regeln sichtbar
Aggregate Root Konsistenzgrenze schützen Order Statuswechsel und Domain Events zentralisieren
Domain Event fachliche Tatsache ausdrücken OrderPlacedEvent, PaymentAuthorizedEvent, InvoiceCreatedEvent entkoppelt Folgeverarbeitung von der Kernaktion
Repository fachliche Persistenz-Schnittstelle OrderRepository Use Case kennt keine Datenbanktechnologie
Ports and Adapters Core von Infrastruktur trennen PaymentGateway, InventoryPort, InvoicePort, OutboxPort Frameworks und Zielsysteme bleiben austauschbar
Application Service Use Case orchestrieren PlaceOrderUseCase koordiniert Domäne, Repository und Outbox
Saga / Process Manager Mehrschrittigen Prozess koordinieren OrderFulfillmentSaga Payment, Inventory, Invoice ohne verteilte ACID-Transaktion
Adapter technische Implementierung eines Ports MockPaymentGateway, ConsoleOutboxAdapter technische Details bleiben außerhalb des Cores
Outbox Pattern Events zuverlässig veröffentlichen OutboxPort + Adapter Produktion würde Event und fachliche Änderung in einer Transaktion persistieren
Quarkus
framework-labs / quarkus-order-api – Überblick

Quarkus Order API

Dieses Projekt zeigt denselben fachlichen Use Case wie order-platform-core, aber mit Quarkus-typischem Adapter-Stil.

Wichtig

Der fachliche Code bleibt im Core. Dieses Projekt enthält nur:

  • HTTP-Schicht
  • Dependency Injection / Wiring
  • Fehlerbehandlung
  • Security- oder Runtime-Hinweise
  • Framework-spezifische Konfiguration

Lernhinweis

Quarkus ist spannend, wenn Container-Startzeit, Memory-Footprint, Kubernetes-Nähe und optional Native Images wichtig sind.

Typische Startbefehle

bash
mvn -pl code/framework-labs/quarkus-order-api -am test
mvn -pl code/framework-labs/quarkus-order-api -am package

Ohne Internetzugriff werden Framework-Abhängigkeiten nicht heruntergeladen. Der JDK-only Core ist im Paket lokal mit javac --release 21 geprüft.

framework-labs / quarkus-order-api – Entwurfsmuster

Design Patterns in Quarkus Order API

Pattern Zweck Einsatzort Begründung
Thin Resource HTTP-Schicht dünn halten OrderResource fachlicher Code bleibt Core
CDI Producer Core in Quarkus verdrahten CoreProducer Adapter bestimmt Runtime-Wiring
Ports and Adapters Quarkus nicht in Domäne ziehen alle Core-Ports Runtime bleibt austauschbar
Spring Boot
framework-labs / spring-boot-order-api – Überblick

Spring Boot Order API

Dieses Projekt zeigt denselben fachlichen Use Case wie order-platform-core, aber mit Spring Boot-typischem Adapter-Stil.

Wichtig

Der fachliche Code bleibt im Core. Dieses Projekt enthält nur:

  • HTTP-Schicht
  • Dependency Injection / Wiring
  • Fehlerbehandlung
  • Security- oder Runtime-Hinweise
  • Framework-spezifische Konfiguration

Lernhinweis

Spring eignet sich gut, wenn viele Enterprise-Integrationen, Security-Bausteine, Actuator, Batch oder Spring-Ökosystem wichtig sind.

Typische Startbefehle

bash
mvn -pl code/framework-labs/spring-boot-order-api -am test
mvn -pl code/framework-labs/spring-boot-order-api -am package

Ohne Internetzugriff werden Framework-Abhängigkeiten nicht heruntergeladen. Der JDK-only Core ist im Paket lokal mit javac --release 21 geprüft.

framework-labs / spring-boot-order-api – Entwurfsmuster

Design Patterns in Spring Boot Order API

Pattern Zweck Einsatzort Begründung
Thin Controller HTTP von Fachlogik trennen OrderController Controller wandelt Request in Command um
Composition Root Beans zentral verdrahten OrderBeansConfig Use Case bleibt frameworkfrei
Error Mapper stabile API-Fehler liefern RestExceptionAdvice interne Exceptions nicht nach außen leaken
Runtime-Modernisierung
runtime-modernization-lab – Überblick

Runtime Modernization Lab 2026

Dieses Lab ergänzt Version 4 des Enterprise-Java-11/17/21-Buchs. Es ist bewusst JDK-only kompilierbar, damit die Architekturregel ohne externe Framework-Downloads geprüft werden kann.

Zweck

Das Lab zeigt, wie Runtime-Entscheidungen als Code modelliert werden können:

  • Runtime-Linien als type-safe Enum
  • Java-Baseline je Runtime
  • Policy Repository
  • Risk Strategy
  • Upgrade Plan Builder
  • Application Service / Facade
  • Dependency Governance Checklist

Kompilieren ohne Maven

bash
find src/main/java -name "*.java" > sources.txt
javac --release 21 -d target/classes @sources.txt
java -cp target/classes com.example.enterprisejava.runtime2026.DemoApplication

Maven-Hinweis

Die pom.xml enthält bewusst zentrale Properties und Profile für Spring Boot 4.x, Jakarta EE 11, Quarkus 3.33 LTS, Micronaut Java-21-Track und Micronaut-5-Java-25-Track. In dieser Umgebung wurde kein Maven-Download ausgeführt.

Wichtigste Regel

text
Java-21-Core bleibt frameworkfrei.
Runtime-Tracks werden als Adapter- und Build-Entscheidung dokumentiert.
runtime-modernization-lab – Entwurfsmuster

Design Patterns im Runtime Modernization Lab

Pattern Zweck Einsatzort Begründung
Type-Safe Enum Runtime-Optionen stabil modellieren RuntimeLine verhindert freie String-Werte und Tippfehler
Immutable Value Object Inventar und Policy reproduzierbar machen ApplicationInventory, RuntimePolicy Modernisierungsentscheidungen werden testbar
Repository Zugriff auf Runtime-Regeln kapseln RuntimePolicyRepository, InMemoryRuntimePolicyRepository spätere DB-/Datei-Quelle austauschbar
Strategy Risikobewertung austauschbar machen RiskClassifier, DefaultRiskClassifier Organisationen bewerten Risiken unterschiedlich
Builder komplexen Upgrade-Plan lesbar erzeugen UpgradePlan.Builder viele optionale Schritte ohne Konstruktorchaos
Application Service / Facade Modernisierungsentscheidung bündeln UpgradePlanner ein sauberer Einstiegspunkt für UI, CLI oder CI
Factory Standard-Wiring zentralisieren RuntimeDecisionFactory Demo bleibt einfach, Wiring bleibt sichtbar
Build Gate Build als Qualitätsgrenze pom.xml Kommentare Modernisierung braucht prüfbare Gates
Supply Chain Evidence Lieferkettennachweis erzeugen pom.xml Kommentare, Checklist SBOM/CVE/Lizenzstatus gehören zur Freigabe
Anhänge
Diagramm-Sammlung

Diagramm-Sammlung

Ddd Order Aggregate

Enterprise Java Landkarte

Final Leseroute

Final Paketstruktur

Final Qualitaetsschleife

Hexagonal Architecture

Migration Java 8 21

Outbox Pattern

Testing Pyramid Enterprise

V2 Framework Landscape

V2 Jakarta Container Flow

V2 Maven Multimodule

V2 Observability Pipeline

V2 Refactoring Split

V2 Security Jwt Flow

V2 Spring Request Flow

V3 Cross Framework Request Flow

V3 Deployment Shapes

V3 Module Boundaries

V3 Portability Map

V3 Shared Core Framework Adapters

V3 Testing Matrix

V4 Dependency Governance Pipeline

V4 Micronaut Baseline Split

V4 Release Train Matrix

V4 Runtime Roadmap 2026

V4 Spring4 Jakarta11 Stack

V4 Upgrade Decision Tree

Virtual Threads

Code-Anhang - wichtige Projektdateien

Code-Anhang - wichtige Projektdateien

code/enterprise-java-platform-lab/pom.xml

xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example.enterprisejava</groupId>
  <artifactId>enterprise-java-platform-lab</artifactId>
  <version>2.0.0</version>
  <name>Enterprise Java Platform Lab</name>
  <properties>
    <maven.compiler.release>21</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.13.0</version>
        <configuration><release>21</release></configuration>
      </plugin>
    </plugins>
  </build>
</project>

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/adapter/ConsoleOutboxPort.java

java
package com.example.enterprisejava.platform.adapter;

import com.example.enterprisejava.platform.application.OutboxPort;
import com.example.enterprisejava.platform.domain.DomainEvent;
import java.util.List;

// Pattern: Outbox Adapter - im echten System wäre dies eine Outbox-Tabelle.
public final class ConsoleOutboxPort implements OutboxPort {
    public void store(List<DomainEvent> events) {
        events.forEach(event -> System.out.println("OUTBOX " + event));
    }
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/adapter/InMemoryOrderRepository.java

java
package com.example.enterprisejava.platform.adapter;

import com.example.enterprisejava.platform.application.OrderRepository;
import com.example.enterprisejava.platform.domain.Order;
import com.example.enterprisejava.platform.domain.OrderId;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

// Pattern: Adapter - technische Speicherung implementiert den Repository Port.
public final class InMemoryOrderRepository implements OrderRepository {
    private final Map<OrderId, Order> store = new ConcurrentHashMap<>();
    public void save(Order order) { store.put(order.id(), order); }
    public Optional<Order> findById(OrderId id) { return Optional.ofNullable(store.get(id)); }
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/adapter/MockPaymentAdapter.java

java
package com.example.enterprisejava.platform.adapter;

import com.example.enterprisejava.platform.application.PaymentPort;
import com.example.enterprisejava.platform.domain.CustomerId;
import com.example.enterprisejava.platform.domain.Money;
import com.example.enterprisejava.platform.domain.PaymentDecision;

// Pattern: Adapter - externe Zahlung wird für das Lab simuliert.
public final class MockPaymentAdapter implements PaymentPort {
    public PaymentDecision authorize(CustomerId customerId, Money total) {
        if (total.amount().intValue() > 10_000) return new PaymentDecision.RequiresManualReview("large order");
        if (customerId.value().startsWith("BLOCKED")) return new PaymentDecision.Rejected("blocked customer");
        return new PaymentDecision.Authorized("AUTH-LAB-" + Math.abs(customerId.value().hashCode()));
    }
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/adapter/RestOrderControllerSimulator.java

java
package com.example.enterprisejava.platform.adapter;

import com.example.enterprisejava.platform.application.PlaceOrderCommand;
import com.example.enterprisejava.platform.application.PlaceOrderUseCase;
import com.example.enterprisejava.platform.domain.OrderId;

// Pattern: Driving Adapter - simuliert REST Controller ohne Framework-Abhängigkeit.
public final class RestOrderControllerSimulator {
    private final PlaceOrderUseCase useCase;
    public RestOrderControllerSimulator(PlaceOrderUseCase useCase) { this.useCase = useCase; }
    public String postOrder(PlaceOrderCommand command) {
        OrderId id = useCase.place(command);
        return "HTTP 201 Created /orders/" + id.value();
    }
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/application/DefaultPricingPolicy.java

java
package com.example.enterprisejava.platform.application;

import com.example.enterprisejava.platform.domain.Money;
import com.example.enterprisejava.platform.domain.OrderLine;
import java.util.List;

// Pattern: Strategy Implementation - Standardpreisregel.
public final class DefaultPricingPolicy implements PricingPolicy {
    @Override
    public Money calculate(List<OrderLine> lines) {
        return lines.stream().map(OrderLine::lineTotal).reduce(Money.eur("0.00"), Money::add);
    }
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/application/OrderQueryFacade.java

java
package com.example.enterprisejava.platform.application;

import com.example.enterprisejava.platform.domain.OrderId;
import com.example.enterprisejava.platform.domain.OrderStatus;

// Pattern: Facade - Lesemodell vereinfacht Zugriff für Adapter.
public final class OrderQueryFacade {
    private final OrderRepository repository;
    public OrderQueryFacade(OrderRepository repository) { this.repository = repository; }
    public String statusOf(OrderId id) {
        return repository.findById(id).map(o -> o.status().name()).orElse(OrderStatus.DRAFT.name());
    }
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/application/OrderRepository.java

java
package com.example.enterprisejava.platform.application;

import com.example.enterprisejava.platform.domain.Order;
import com.example.enterprisejava.platform.domain.OrderId;
import java.util.Optional;

// Pattern: Repository Port - Anwendung hängt nicht an JPA/JDBC.
public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(OrderId id);
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/application/OutboxPort.java

java
package com.example.enterprisejava.platform.application;

import com.example.enterprisejava.platform.domain.DomainEvent;
import java.util.List;

// Pattern: Outbox Port - Event-Veröffentlichung wird transaktionsnah kapselbar.
public interface OutboxPort {
    void store(List<DomainEvent> events);
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/application/PaymentPort.java

java
package com.example.enterprisejava.platform.application;

import com.example.enterprisejava.platform.domain.CustomerId;
import com.example.enterprisejava.platform.domain.Money;
import com.example.enterprisejava.platform.domain.PaymentDecision;

// Pattern: Port - externe Zahlung wird als fachliche Schnittstelle modelliert.
public interface PaymentPort {
    PaymentDecision authorize(CustomerId customerId, Money total);
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/application/PlaceOrderCommand.java

java
package com.example.enterprisejava.platform.application;

import com.example.enterprisejava.platform.domain.CustomerId;
import com.example.enterprisejava.platform.domain.OrderLine;
import java.util.List;

// Pattern: Command DTO - stabiler Eingang in den Use Case.
public record PlaceOrderCommand(CustomerId customerId, List<OrderLine> lines) {}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/application/PlaceOrderUseCase.java

java
package com.example.enterprisejava.platform.application;

import com.example.enterprisejava.platform.domain.Order;
import com.example.enterprisejava.platform.domain.OrderId;
import com.example.enterprisejava.platform.domain.PaymentDecision;

// Pattern: Application Service / Use Case - orchestriert den fachlichen Ablauf.
public final class PlaceOrderUseCase {
    private final OrderRepository repository;
    private final PaymentPort paymentPort;
    private final OutboxPort outboxPort;
    private final PricingPolicy pricingPolicy;

    public PlaceOrderUseCase(OrderRepository repository, PaymentPort paymentPort, OutboxPort outboxPort, PricingPolicy pricingPolicy) {
        this.repository = repository;
        this.paymentPort = paymentPort;
        this.outboxPort = outboxPort;
        this.pricingPolicy = pricingPolicy;
    }

    public OrderId place(PlaceOrderCommand command) {
        var total = pricingPolicy.calculate(command.lines());
        Order order = Order.place(command.customerId(), command.lines());
        PaymentDecision decision = paymentPort.authorize(command.customerId(), total);
        order.applyPayment(decision);
        repository.save(order);
        outboxPort.store(order.pullEvents());
        return order.id();
    }
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/application/PricingPolicy.java

java
package com.example.enterprisejava.platform.application;

import com.example.enterprisejava.platform.domain.Money;
import com.example.enterprisejava.platform.domain.OrderLine;
import java.util.List;

// Pattern: Strategy - Preisberechnung kann je Kontext ausgetauscht werden.
public interface PricingPolicy {
    Money calculate(List<OrderLine> lines);
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/domain/CustomerId.java

java
package com.example.enterprisejava.platform.domain;

// Pattern: Value Object - fachlicher Typ statt nackter String.
public record CustomerId(String value) {
    public CustomerId {
        if (value == null || value.isBlank()) throw new IllegalArgumentException("customer id missing");
    }
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/domain/DomainEvent.java

java
package com.example.enterprisejava.platform.domain;

import java.time.Instant;

// Pattern: Domain Event - fachlich relevante Tatsache wird explizit publizierbar.
public sealed interface DomainEvent permits OrderPlacedEvent {
    Instant occurredAt();
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/domain/Money.java

java
package com.example.enterprisejava.platform.domain;

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

// Pattern: Value Object - Geldbetrag ist unveränderlich und schützt fachliche Regeln.
public record Money(BigDecimal amount, Currency currency) {
    public Money {
        Objects.requireNonNull(amount, "amount");
        Objects.requireNonNull(currency, "currency");
        amount = amount.setScale(2, RoundingMode.HALF_UP);
        if (amount.signum() < 0) throw new IllegalArgumentException("Money must not be negative");
    }
    public static Money eur(String value) { return new Money(new BigDecimal(value), Currency.getInstance("EUR")); }
    public Money add(Money other) { requireSameCurrency(other); return new Money(amount.add(other.amount), currency); }
    public Money multiply(int factor) { return new Money(amount.multiply(BigDecimal.valueOf(factor)), currency); }
    private void requireSameCurrency(Money other) {
        if (!currency.equals(other.currency())) throw new IllegalArgumentException("Currency mismatch");
    }
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/domain/Order.java

java
package com.example.enterprisejava.platform.domain;

import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

// Pattern: Aggregate Root - Order schützt Invarianten und erzeugt Domain Events.
public final class Order {
    private final OrderId id;
    private final CustomerId customerId;
    private final List<OrderLine> lines;
    private final List<DomainEvent> events = new ArrayList<>();
    private OrderStatus status;

    private Order(OrderId id, CustomerId customerId, List<OrderLine> lines) {
        if (lines == null || lines.isEmpty()) throw new IllegalArgumentException("order must contain lines");
        this.id = id;
        this.customerId = customerId;
        this.lines = List.copyOf(lines);
        this.status = OrderStatus.DRAFT;
    }

    public static Order place(CustomerId customerId, List<OrderLine> lines) {
        Order order = new Order(OrderId.newId(), customerId, lines);
        order.status = OrderStatus.PLACED;
        order.events.add(new OrderPlacedEvent(order.id, order.customerId, order.total(), Instant.now()));
        return order;
    }

    public void applyPayment(PaymentDecision decision) {
        switch (decision) {
            case PaymentDecision.Authorized ignored -> status = OrderStatus.PAYMENT_AUTHORIZED;
            case PaymentDecision.Rejected ignored -> status = OrderStatus.PAYMENT_REJECTED;
            case PaymentDecision.RequiresManualReview ignored -> status = OrderStatus.MANUAL_REVIEW;
        }
    }

    public Money total() {
        return lines.stream().map(OrderLine::lineTotal).reduce(Money.eur("0.00"), Money::add);
    }

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

    public OrderId id() { return id; }
    public CustomerId customerId() { return customerId; }
    public List<OrderLine> lines() { return Collections.unmodifiableList(lines); }
    public OrderStatus status() { return status; }
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/domain/OrderId.java

java
package com.example.enterprisejava.platform.domain;

import java.util.UUID;

// Pattern: Value Object - verhindert Verwechslung von technischen IDs.
public record OrderId(UUID value) {
    public static OrderId newId() { return new OrderId(UUID.randomUUID()); }
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/domain/OrderLine.java

java
package com.example.enterprisejava.platform.domain;

// Pattern: Value Object - OrderLine ist unveränderlicher Bestandteil des Aggregates.
public record OrderLine(String sku, int quantity, Money unitPrice) {
    public OrderLine {
        if (sku == null || sku.isBlank()) throw new IllegalArgumentException("sku missing");
        if (quantity <= 0) throw new IllegalArgumentException("quantity must be positive");
    }
    public Money lineTotal() { return unitPrice.multiply(quantity); }
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/domain/OrderPlacedEvent.java

java
package com.example.enterprisejava.platform.domain;

import java.time.Instant;

public record OrderPlacedEvent(OrderId orderId, CustomerId customerId, Money total, Instant occurredAt) implements DomainEvent {}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/domain/OrderStatus.java

java
package com.example.enterprisejava.platform.domain;

public enum OrderStatus { DRAFT, PLACED, PAYMENT_AUTHORIZED, PAYMENT_REJECTED, MANUAL_REVIEW }

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/domain/PaymentDecision.java

java
package com.example.enterprisejava.platform.domain;

// Pattern: Result Pattern mit sealed types - kontrollierte fachliche Alternativen.
public sealed interface PaymentDecision permits PaymentDecision.Authorized, PaymentDecision.Rejected, PaymentDecision.RequiresManualReview {
    record Authorized(String authorizationCode) implements PaymentDecision {}
    record Rejected(String reason) implements PaymentDecision {}
    record RequiresManualReview(String reason) implements PaymentDecision {}
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/refactoring/LegacyOrderProcessorBefore.java

java
package com.example.enterprisejava.platform.refactoring;

import java.math.BigDecimal;
import java.util.List;

// Absichtlich schlechtes Beispiel: Monster Method Anti-Pattern.
public final class LegacyOrderProcessorBefore {
    public String process(String customerId, List<BigDecimal> prices, boolean vip) {
        if (customerId == null || customerId.isBlank()) return "ERROR_CUSTOMER";
        if (prices == null || prices.isEmpty()) return "ERROR_LINES";
        BigDecimal total = BigDecimal.ZERO;
        for (BigDecimal price : prices) {
            if (price == null || price.signum() <= 0) return "ERROR_PRICE";
            total = total.add(price);
        }
        if (vip) total = total.multiply(new BigDecimal("0.90"));
        if (total.compareTo(new BigDecimal("10000")) > 0) return "MANUAL_REVIEW";
        return "CREATED:" + total;
    }
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/refactoring/RefactoredOrderProcessorAfter.java

java
package com.example.enterprisejava.platform.refactoring;

import java.math.BigDecimal;
import java.util.List;

// Pattern: Template Method light / Extracted Methods - Schritte sind getrennt und testbar.
public final class RefactoredOrderProcessorAfter {
    public String process(String customerId, List<BigDecimal> prices, boolean vip) {
        validate(customerId, prices);
        BigDecimal total = applyDiscount(sum(prices), vip);
        return decide(total);
    }
    private void validate(String customerId, List<BigDecimal> prices) {
        if (customerId == null || customerId.isBlank()) throw new IllegalArgumentException("customer missing");
        if (prices == null || prices.isEmpty()) throw new IllegalArgumentException("lines missing");
        if (prices.stream().anyMatch(p -> p == null || p.signum() <= 0)) throw new IllegalArgumentException("invalid price");
    }
    private BigDecimal sum(List<BigDecimal> prices) { return prices.stream().reduce(BigDecimal.ZERO, BigDecimal::add); }
    private BigDecimal applyDiscount(BigDecimal total, boolean vip) { return vip ? total.multiply(new BigDecimal("0.90")) : total; }
    private String decide(BigDecimal total) { return total.compareTo(new BigDecimal("10000")) > 0 ? "MANUAL_REVIEW" : "CREATED:" + total; }
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/support/DemoApplication.java

java
package com.example.enterprisejava.platform.support;

import com.example.enterprisejava.platform.adapter.ConsoleOutboxPort;
import com.example.enterprisejava.platform.adapter.InMemoryOrderRepository;
import com.example.enterprisejava.platform.adapter.MockPaymentAdapter;
import com.example.enterprisejava.platform.adapter.RestOrderControllerSimulator;
import com.example.enterprisejava.platform.application.DefaultPricingPolicy;
import com.example.enterprisejava.platform.application.PlaceOrderCommand;
import com.example.enterprisejava.platform.application.PlaceOrderUseCase;
import com.example.enterprisejava.platform.domain.CustomerId;
import com.example.enterprisejava.platform.domain.Money;
import com.example.enterprisejava.platform.domain.OrderLine;
import java.util.List;

public final class DemoApplication {
    public static void main(String[] args) {
        var repository = new InMemoryOrderRepository();
        var useCase = new PlaceOrderUseCase(repository, new MockPaymentAdapter(), new ConsoleOutboxPort(), new DefaultPricingPolicy());
        var controller = new RestOrderControllerSimulator(useCase);
        var command = new PlaceOrderCommand(new CustomerId("C-1000"), List.of(new OrderLine("SKU-1", 2, Money.eur("19.90"))));
        System.out.println(controller.postOrder(command));
    }
}

code/enterprise-java-platform-lab/src/main/java/com/example/enterprisejava/platform/support/VirtualThreadsBatchClient.java

java
package com.example.enterprisejava.platform.support;

import java.util.List;
import java.util.concurrent.Executors;

// Java 21 Beispiel: Virtual Threads für viele blockierende, unabhängige Aufgaben.
public final class VirtualThreadsBatchClient {
    public List<String> loadAll(List<String> ids) throws InterruptedException {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            var futures = ids.stream().map(id -> executor.submit(() -> loadOne(id))).toList();
            return futures.stream().map(f -> {
                try { return f.get(); }
                catch (Exception e) { throw new IllegalStateException(e); }
            }).toList();
        }
    }
    private String loadOne(String id) throws InterruptedException {
        Thread.sleep(10); // simuliert blockierendes I/O
        return "remote-data-" + id;
    }
}

code/enterprise-order-billing-demo/pom.xml

xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example.enterprisejava</groupId>
  <artifactId>enterprise-order-billing-demo</artifactId>
  <version>1.0.0</version>
  <name>Enterprise Order Billing Demo</name>
  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.13.0</version>
        <configuration>
          <release>${maven.compiler.release}</release>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

code/enterprise-order-billing-demo/src/main/java/com/example/enterprisejava/orderbilling/DemoApplication.java

java
package com.example.enterprisejava.orderbilling;

import com.example.enterprisejava.orderbilling.adapter.*;
import com.example.enterprisejava.orderbilling.application.*;
import com.example.enterprisejava.orderbilling.domain.*;
import java.util.List;

public final class DemoApplication {
    public static void main(String[] args) {
        var useCase = new PlaceOrderUseCase(
                new InMemoryOrderRepository(),
                new MockPaymentAdapter(),
                new ConsoleOutboxPublisher()
        );

        var command = new PlaceOrderCommand("C-1000", List.of(
                new OrderLine("SKU-BOOK", 2, Money.eur("39.90")),
                new OrderLine("SKU-SUPPORT", 1, Money.eur("99.00"))
        ));

        System.out.println("Created order " + useCase.place(command));
    }
}

code/enterprise-order-billing-demo/src/main/java/com/example/enterprisejava/orderbilling/adapter/ConsoleOutboxPublisher.java

java
package com.example.enterprisejava.orderbilling.adapter;

import com.example.enterprisejava.orderbilling.application.OutboxPublisher;
import com.example.enterprisejava.orderbilling.domain.DomainEvent;
import java.util.List;

/**
 * Outbox Pattern Demo-Adapter.
 * Zweck: in echter Anwendung in Outbox-Tabelle schreiben; hier nur Ausgabe.
 */
public final class ConsoleOutboxPublisher implements OutboxPublisher {
    @Override
    public void stage(List<DomainEvent> events) {
        events.forEach(event -> System.out.println("OUTBOX STAGED " + event));
    }
}

code/enterprise-order-billing-demo/src/main/java/com/example/enterprisejava/orderbilling/adapter/InMemoryOrderRepository.java

java
package com.example.enterprisejava.orderbilling.adapter;

import com.example.enterprisejava.orderbilling.application.OrderRepository;
import com.example.enterprisejava.orderbilling.domain.Order;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Adapter Pattern.
 * Zweck: technische Repository-Implementierung austauschbar halten.
 */
public final class InMemoryOrderRepository implements OrderRepository {
    private final Map<UUID, Order> orders = new ConcurrentHashMap<>();

    @Override
    public Optional<Order> findById(UUID id) {
        return Optional.ofNullable(orders.get(id));
    }

    @Override
    public void save(Order order) {
        orders.put(order.id(), order);
    }
}

code/enterprise-order-billing-demo/src/main/java/com/example/enterprisejava/orderbilling/adapter/MockPaymentAdapter.java

java
package com.example.enterprisejava.orderbilling.adapter;

import com.example.enterprisejava.orderbilling.application.PaymentPort;
import com.example.enterprisejava.orderbilling.domain.*;

/**
 * Adapter Pattern.
 * Zweck: simuliert externes Payment-System hinter dem PaymentPort.
 */
public final class MockPaymentAdapter implements PaymentPort {
    @Override
    public PaymentDecision authorize(Order order) {
        if (order.rawTotal().amount().signum() <= 0) {
            return new PaymentDecision.Rejected("INVALID_AMOUNT", "Amount must be positive");
        }
        return new PaymentDecision.Authorized("AUTH-DEMO-" + order.id());
    }
}

code/enterprise-order-billing-demo/src/main/java/com/example/enterprisejava/orderbilling/application/OrderRepository.java

java
package com.example.enterprisejava.orderbilling.application;

import com.example.enterprisejava.orderbilling.domain.Order;
import java.util.Optional;
import java.util.UUID;

/**
 * Repository Pattern.
 * Zweck: Persistenzschnittstelle im Application Core; technische JPA/SQL-Details bleiben Adapter.
 */
public interface OrderRepository {
    Optional<Order> findById(UUID id);
    void save(Order order);
}

code/enterprise-order-billing-demo/src/main/java/com/example/enterprisejava/orderbilling/application/OutboxPublisher.java

java
package com.example.enterprisejava.orderbilling.application;

import com.example.enterprisejava.orderbilling.domain.DomainEvent;
import java.util.List;

/**
 * Outbox Pattern.
 * Zweck: Events zunächst transaktional vormerken, später zuverlässig veröffentlichen.
 */
public interface OutboxPublisher {
    void stage(List<DomainEvent> events);
}

code/enterprise-order-billing-demo/src/main/java/com/example/enterprisejava/orderbilling/application/PaymentPort.java

java
package com.example.enterprisejava.orderbilling.application;

import com.example.enterprisejava.orderbilling.domain.Order;
import com.example.enterprisejava.orderbilling.domain.PaymentDecision;

/**
 * Port in Hexagonal Architecture.
 * Zweck: Application Core kennt nur fachliche Schnittstelle, nicht den HTTP Adapter.
 */
public interface PaymentPort {
    PaymentDecision authorize(Order order);
}

code/enterprise-order-billing-demo/src/main/java/com/example/enterprisejava/orderbilling/application/PlaceOrderCommand.java

java
package com.example.enterprisejava.orderbilling.application;

import com.example.enterprisejava.orderbilling.domain.OrderLine;
import java.util.List;

/** Java 17 Record als unveränderliches Command DTO. */
public record PlaceOrderCommand(String customerNumber, List<OrderLine> lines) {
    public PlaceOrderCommand {
        if (customerNumber == null || customerNumber.isBlank()) throw new IllegalArgumentException("customerNumber is required");
        lines = List.copyOf(lines);
    }
}

code/enterprise-order-billing-demo/src/main/java/com/example/enterprisejava/orderbilling/application/PlaceOrderUseCase.java

java
package com.example.enterprisejava.orderbilling.application;

import com.example.enterprisejava.orderbilling.domain.Order;
import com.example.enterprisejava.orderbilling.domain.PaymentDecision;
import java.util.UUID;

/**
 * Facade Pattern / Application Service.
 * Zweck: Ein klarer Use Case orchestriert Domain, Ports und Persistenz.
 */
public final class PlaceOrderUseCase {
    private final OrderRepository orderRepository;
    private final PaymentPort paymentPort;
    private final OutboxPublisher outboxPublisher;

    public PlaceOrderUseCase(OrderRepository orderRepository, PaymentPort paymentPort, OutboxPublisher outboxPublisher) {
        this.orderRepository = orderRepository;
        this.paymentPort = paymentPort;
        this.outboxPublisher = outboxPublisher;
    }

    public UUID place(PlaceOrderCommand command) {
        Order order = Order.create(command.customerNumber(), command.lines()); // Factory Method im Aggregate
        PaymentDecision decision = paymentPort.authorize(order);              // Port/Adapter Pattern
        order.apply(decision);                                                // fachliche Entscheidung im Aggregate
        orderRepository.save(order);                                          // Repository Pattern
        outboxPublisher.stage(order.pullDomainEvents());                       // Outbox Pattern
        return order.id();
    }
}

code/enterprise-order-billing-demo/src/main/java/com/example/enterprisejava/orderbilling/application/PricingStrategy.java

java
package com.example.enterprisejava.orderbilling.application;

import com.example.enterprisejava.orderbilling.domain.Money;
import com.example.enterprisejava.orderbilling.domain.Order;

/**
 * Strategy Pattern.
 * Zweck: Preislogik nach Kundentyp austauschbar machen.
 */
public interface PricingStrategy {
    Money calculate(Order order);
}

code/enterprise-order-billing-demo/src/main/java/com/example/enterprisejava/orderbilling/domain/DomainEvent.java

java
package com.example.enterprisejava.orderbilling.domain;

import java.time.Instant;
import java.util.UUID;

/**
 * Domain Event Pattern.
 * Zweck: fachliche Ereignisse explizit machen, ohne direkt technische Broker zu kennen.
 */
public interface DomainEvent {
    UUID eventId();
    Instant occurredAt();
}

code/enterprise-order-billing-demo/src/main/java/com/example/enterprisejava/orderbilling/domain/Money.java

java
package com.example.enterprisejava.orderbilling.domain;

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

/**
 * Value Object Pattern.
 * Zweck: Geldbetrag unveränderlich und fachlich sicher modellieren.
 */
public record Money(BigDecimal amount, Currency currency) {
    public Money {
        if (amount == null || currency == null) {
            throw new IllegalArgumentException("amount and currency are required");
        }
        amount = amount.setScale(2, RoundingMode.HALF_UP);
    }

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

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

    public Money minusPercent(int percent) {
        BigDecimal factor = BigDecimal.valueOf(100 - percent).divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP);
        return new Money(amount.multiply(factor), currency);
    }

    private void requireSameCurrency(Money other) {
        if (!currency.equals(other.currency)) {
            throw new IllegalArgumentException("currency mismatch");
        }
    }
}

code/enterprise-order-billing-demo/src/main/java/com/example/enterprisejava/orderbilling/domain/Order.java

java
package com.example.enterprisejava.orderbilling.domain;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * Aggregate Root Pattern.
 * Zweck: Order schützt Invarianten der Bestellung und sammelt Domain Events.
 */
public final class Order {
    private final UUID id;
    private final String customerNumber;
    private final List<OrderLine> lines;
    private final List<DomainEvent> events = new ArrayList<>();
    private OrderStatus status;

    private Order(UUID id, String customerNumber, List<OrderLine> lines) {
        this.id = id;
        this.customerNumber = customerNumber;
        this.lines = List.copyOf(lines);
        this.status = OrderStatus.CREATED;
        this.events.add(OrderPlacedEvent.now(id, customerNumber, rawTotal()));
    }

    /** Factory Method Pattern: zentrale und validierte Erzeugung. */
    public static Order create(String customerNumber, List<OrderLine> lines) {
        if (customerNumber == null || customerNumber.isBlank()) throw new IllegalArgumentException("customerNumber is required");
        if (lines == null || lines.isEmpty()) throw new IllegalArgumentException("at least one line is required");
        return new Order(UUID.randomUUID(), customerNumber, lines);
    }

    public void apply(PaymentDecision decision) {
        if (decision instanceof PaymentDecision.Authorized) {
            this.status = OrderStatus.PAYMENT_AUTHORIZED;
        } else if (decision instanceof PaymentDecision.Rejected) {
            this.status = OrderStatus.PAYMENT_REJECTED;
        } else if (decision instanceof PaymentDecision.RequiresManualReview) {
            this.status = OrderStatus.MANUAL_REVIEW;
        } else {
            throw new IllegalArgumentException("unknown payment decision");
        }
    }

    public Money rawTotal() {
        Money result = Money.eur("0.00");
        for (OrderLine line : lines) {
            result = result.plus(line.lineTotal());
        }
        return result;
    }

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

    public UUID id() { return id; }
    public String customerNumber() { return customerNumber; }
    public OrderStatus status() { return status; }
}

code/enterprise-order-billing-demo/src/main/java/com/example/enterprisejava/orderbilling/domain/OrderLine.java

java
package com.example.enterprisejava.orderbilling.domain;

/**
 * Value Object Pattern.
 * Zweck: Bestellposition ist unveränderlich und validiert sich selbst.
 */
public record OrderLine(String sku, int quantity, Money unitPrice) {
    public OrderLine {
        if (sku == null || sku.isBlank()) throw new IllegalArgumentException("sku is required");
        if (quantity <= 0) throw new IllegalArgumentException("quantity must be positive");
        if (unitPrice == null) throw new IllegalArgumentException("unitPrice is required");
    }

    public Money lineTotal() {
        Money total = Money.eur("0.00");
        for (int i = 0; i < quantity; i++) {
            total = total.plus(unitPrice);
        }
        return total;
    }
}

code/enterprise-order-billing-demo/src/main/java/com/example/enterprisejava/orderbilling/domain/OrderPlacedEvent.java

java
package com.example.enterprisejava.orderbilling.domain;

import java.time.Instant;
import java.util.UUID;

/**
 * Domain Event Pattern.
 * Einsatzort: Order Aggregate veröffentlicht fachliches Ereignis nach erfolgreicher Anlage.
 */
public record OrderPlacedEvent(
        UUID eventId,
        Instant occurredAt,
        UUID orderId,
        String customerNumber,
        Money total
) implements DomainEvent {
    public static OrderPlacedEvent now(UUID orderId, String customerNumber, Money total) {
        return new OrderPlacedEvent(UUID.randomUUID(), Instant.now(), orderId, customerNumber, total);
    }
}

code/enterprise-order-billing-demo/src/main/java/com/example/enterprisejava/orderbilling/domain/OrderStatus.java

java
package com.example.enterprisejava.orderbilling.domain;

public enum OrderStatus {
    CREATED,
    PAYMENT_AUTHORIZED,
    PAYMENT_REJECTED,
    MANUAL_REVIEW
}

code/enterprise-order-billing-demo/src/main/java/com/example/enterprisejava/orderbilling/domain/PaymentDecision.java

java
package com.example.enterprisejava.orderbilling.domain;

/**
 * Result Pattern mit sealed interface.
 * Zweck: erwartete Payment-Ergebnisse explizit typisieren.
 */
public sealed interface PaymentDecision
        permits PaymentDecision.Authorized, PaymentDecision.Rejected, PaymentDecision.RequiresManualReview {

    record Authorized(String authorizationId) implements PaymentDecision {}
    record Rejected(String reasonCode, String message) implements PaymentDecision {}
    record RequiresManualReview(String ticketId) implements PaymentDecision {}
}

code/framework-labs/jakarta-ee-order-api/pom.xml

xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent><groupId>com.example.enterprisejava</groupId><artifactId>framework-labs-parent</artifactId><version>1.0.0-SNAPSHOT</version></parent>
  <artifactId>jakarta-ee-order-api</artifactId>
  <packaging>war</packaging>
  <dependencies>
    <dependency><groupId>com.example.enterprisejava</groupId><artifactId>order-platform-core</artifactId><version>${project.version}</version></dependency>
    <dependency><groupId>jakarta.platform</groupId><artifactId>jakarta.jakartaee-api</artifactId><version>${jakarta.ee.version}</version><scope>provided</scope></dependency>
  </dependencies>
</project>

code/framework-labs/jakarta-ee-order-api/src/main/java/com/example/enterprisejava/frameworklab/jakarta/api/JaxRsApplication.java

java
package com.example.enterprisejava.frameworklab.jakarta.api;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("/api")
public class JaxRsApplication extends Application {}

code/framework-labs/jakarta-ee-order-api/src/main/java/com/example/enterprisejava/frameworklab/jakarta/api/OrderResource.java

java
package com.example.enterprisejava.frameworklab.jakarta.api;

import com.example.enterprisejava.frameworklab.core.application.PlaceOrderCommand;
import com.example.enterprisejava.frameworklab.core.application.PlaceOrderUseCase;
import com.example.enterprisejava.frameworklab.core.domain.CustomerId;
import com.example.enterprisejava.frameworklab.core.domain.Money;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;

// Pattern: Resource Adapter - JAX-RS übersetzt HTTP in Use-Case-Command.
@Path("/orders")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class OrderResource {
    @Inject PlaceOrderUseCase placeOrderUseCase;
    @POST
    public Response place(OrderRequest request) {
        var command = new PlaceOrderCommand(new CustomerId(request.customerId()),
                request.lines().stream().map(line -> new PlaceOrderCommand.Line(line.sku(), line.quantity(), Money.eur(line.unitPrice()))).toList());
        var receipt = placeOrderUseCase.place(command);
        return Response.status(Response.Status.CREATED).entity(new OrderResponse(receipt.orderId().toString(), receipt.status().name(), receipt.total().toString())).build();
    }
    public record OrderRequest(String customerId, List<LineRequest> lines) {}
    public record LineRequest(String sku, int quantity, String unitPrice) {}
    public record OrderResponse(String orderId, String status, String total) {}
}

code/framework-labs/jakarta-ee-order-api/src/main/java/com/example/enterprisejava/frameworklab/jakarta/config/CoreProducer.java

java
package com.example.enterprisejava.frameworklab.jakarta.config;

import com.example.enterprisejava.frameworklab.core.adapter.ConsoleOutboxAdapter;
import com.example.enterprisejava.frameworklab.core.adapter.InMemoryOrderRepository;
import com.example.enterprisejava.frameworklab.core.application.PlaceOrderUseCase;
import com.example.enterprisejava.frameworklab.core.application.port.OrderRepository;
import com.example.enterprisejava.frameworklab.core.application.port.OutboxPort;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;

// Pattern: Composition Root mit CDI Producer - Container erstellt Core-Bausteine.
@ApplicationScoped
public class CoreProducer {
    @Produces @ApplicationScoped OrderRepository orderRepository() { return new InMemoryOrderRepository(); }
    @Produces @ApplicationScoped OutboxPort outboxPort() { return new ConsoleOutboxAdapter(); }
    @Produces @ApplicationScoped PlaceOrderUseCase placeOrderUseCase(OrderRepository repo, OutboxPort outbox) { return new PlaceOrderUseCase(repo, outbox); }
}

code/framework-labs/jakarta-ee-order-api/src/main/java/com/example/enterprisejava/frameworklab/jakarta/error/BusinessExceptionMapper.java

java
package com.example.enterprisejava.frameworklab.jakarta.error;

import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

// Pattern: Exception Mapper - JAX-RS-spezifische Fehlerübersetzung.
@Provider
public class BusinessExceptionMapper implements ExceptionMapper<IllegalArgumentException> {
    @Override public Response toResponse(IllegalArgumentException ex) {
        return Response.status(Response.Status.BAD_REQUEST).entity(new ApiError("BUSINESS_RULE_VIOLATION", ex.getMessage())).build();
    }
    public record ApiError(String code, String message) {}
}

code/framework-labs/micronaut-order-api/pom.xml

xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent><groupId>com.example.enterprisejava</groupId><artifactId>framework-labs-parent</artifactId><version>1.0.0-SNAPSHOT</version></parent>
  <artifactId>micronaut-order-api</artifactId>
  <dependencies>
    <dependency><groupId>com.example.enterprisejava</groupId><artifactId>order-platform-core</artifactId><version>${project.version}</version></dependency>
    <dependency><groupId>io.micronaut</groupId><artifactId>micronaut-http-server-netty</artifactId><version>${micronaut.version}</version></dependency>
    <dependency><groupId>io.micronaut.serde</groupId><artifactId>micronaut-serde-jackson</artifactId><version>${micronaut.version}</version></dependency>
  </dependencies>
</project>

code/framework-labs/micronaut-order-api/src/main/java/com/example/enterprisejava/frameworklab/micronaut/MicronautOrderApplication.java

java
package com.example.enterprisejava.frameworklab.micronaut;

import io.micronaut.runtime.Micronaut;

public class MicronautOrderApplication {
    public static void main(String[] args) { Micronaut.run(MicronautOrderApplication.class, args); }
}

code/framework-labs/micronaut-order-api/src/main/java/com/example/enterprisejava/frameworklab/micronaut/api/OrderController.java

java
package com.example.enterprisejava.frameworklab.micronaut.api;

import com.example.enterprisejava.frameworklab.core.application.PlaceOrderCommand;
import com.example.enterprisejava.frameworklab.core.application.PlaceOrderUseCase;
import com.example.enterprisejava.frameworklab.core.domain.CustomerId;
import com.example.enterprisejava.frameworklab.core.domain.Money;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.*;
import java.util.List;

// Pattern: Thin Controller - Micronaut-spezifische Annotationen bleiben in der Adapter-Schicht.
@Controller("/orders")
public class OrderController {
    private final PlaceOrderUseCase placeOrderUseCase;
    public OrderController(PlaceOrderUseCase placeOrderUseCase) { this.placeOrderUseCase = placeOrderUseCase; }
    @Post
    public HttpResponse<OrderResponse> place(@Body OrderRequest request) {
        var command = new PlaceOrderCommand(new CustomerId(request.customerId()),
                request.lines().stream().map(line -> new PlaceOrderCommand.Line(line.sku(), line.quantity(), Money.eur(line.unitPrice()))).toList());
        var receipt = placeOrderUseCase.place(command);
        return HttpResponse.created(new OrderResponse(receipt.orderId().toString(), receipt.status().name(), receipt.total().toString()));
    }
    public record OrderRequest(String customerId, List<LineRequest> lines) {}
    public record LineRequest(String sku, int quantity, String unitPrice) {}
    public record OrderResponse(String orderId, String status, String total) {}
}

code/framework-labs/micronaut-order-api/src/main/java/com/example/enterprisejava/frameworklab/micronaut/config/CoreFactory.java

java
package com.example.enterprisejava.frameworklab.micronaut.config;

import com.example.enterprisejava.frameworklab.core.adapter.ConsoleOutboxAdapter;
import com.example.enterprisejava.frameworklab.core.adapter.InMemoryOrderRepository;
import com.example.enterprisejava.frameworklab.core.application.PlaceOrderUseCase;
import com.example.enterprisejava.frameworklab.core.application.port.OrderRepository;
import com.example.enterprisejava.frameworklab.core.application.port.OutboxPort;
import io.micronaut.context.annotation.Factory;
import jakarta.inject.Singleton;

// Pattern: Composition Root - Micronaut Factory erzeugt Core-Bausteine mit compile-time DI.
@Factory
public class CoreFactory {
    @Singleton OrderRepository orderRepository() { return new InMemoryOrderRepository(); }
    @Singleton OutboxPort outboxPort() { return new ConsoleOutboxAdapter(); }
    @Singleton PlaceOrderUseCase placeOrderUseCase(OrderRepository repo, OutboxPort outbox) { return new PlaceOrderUseCase(repo, outbox); }
}

code/framework-labs/order-platform-core/pom.xml

xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent><groupId>com.example.enterprisejava</groupId><artifactId>framework-labs-parent</artifactId><version>1.0.0-SNAPSHOT</version></parent>
  <artifactId>order-platform-core</artifactId>
  <packaging>jar</packaging>
</project>

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/adapter/ConsoleOutboxAdapter.java

java
package com.example.enterprisejava.frameworklab.core.adapter;

import com.example.enterprisejava.frameworklab.core.application.port.OutboxPort;
import com.example.enterprisejava.frameworklab.core.domain.DomainEvent;

// Pattern: Adapter + Outbox Pattern Demo - Produktion würde hier Outbox-Tabelle verwenden.
public final class ConsoleOutboxAdapter implements OutboxPort {
    @Override public void append(DomainEvent event) { System.out.println("OUTBOX " + event.eventType() + " " + event.orderId()); }
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/adapter/InMemoryOrderRepository.java

java
package com.example.enterprisejava.frameworklab.core.adapter;

import com.example.enterprisejava.frameworklab.core.application.port.OrderRepository;
import com.example.enterprisejava.frameworklab.core.domain.Order;
import com.example.enterprisejava.frameworklab.core.domain.OrderId;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

// Pattern: Adapter - einfache technische Implementierung eines Repository Ports.
public final class InMemoryOrderRepository implements OrderRepository {
    private final Map<OrderId, Order> orders = new ConcurrentHashMap<>();
    @Override public void save(Order order) { orders.put(order.id(), order); }
    @Override public Optional<Order> findById(OrderId id) { return Optional.ofNullable(orders.get(id)); }
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/adapter/MockInventoryAdapter.java

java
package com.example.enterprisejava.frameworklab.core.adapter;

import com.example.enterprisejava.frameworklab.core.application.port.InventoryPort;
import com.example.enterprisejava.frameworklab.core.domain.OrderLine;
import java.util.List;

// Pattern: Adapter - Lagerreservierung bleibt austauschbar.
public final class MockInventoryAdapter implements InventoryPort {
    @Override public void reserve(List<OrderLine> lines) { System.out.println("RESERVE " + lines.size() + " lines"); }
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/adapter/MockInvoiceAdapter.java

java
package com.example.enterprisejava.frameworklab.core.adapter;

import com.example.enterprisejava.frameworklab.core.application.port.InvoicePort;
import com.example.enterprisejava.frameworklab.core.domain.Order;

// Pattern: Adapter - Rechnungsnummer kommt aus einem austauschbaren Zielsystem.
public final class MockInvoiceAdapter implements InvoicePort {
    @Override public String createInvoice(Order order) { return "INV-" + order.id().toString().substring(0, 8); }
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/adapter/MockPaymentGateway.java

java
package com.example.enterprisejava.frameworklab.core.adapter;

import com.example.enterprisejava.frameworklab.core.application.port.PaymentGateway;
import com.example.enterprisejava.frameworklab.core.domain.Money;
import com.example.enterprisejava.frameworklab.core.domain.OrderId;

// Pattern: Adapter - simuliert externe Payment-Plattform hinter einem Port.
public final class MockPaymentGateway implements PaymentGateway {
    @Override public String authorize(OrderId orderId, Money amount) { return "AUTH-" + orderId.toString().substring(0, 8); }
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/application/OrderFulfillmentSaga.java

java
package com.example.enterprisejava.frameworklab.core.application;

import com.example.enterprisejava.frameworklab.core.application.port.InventoryPort;
import com.example.enterprisejava.frameworklab.core.application.port.InvoicePort;
import com.example.enterprisejava.frameworklab.core.application.port.OrderRepository;
import com.example.enterprisejava.frameworklab.core.application.port.OutboxPort;
import com.example.enterprisejava.frameworklab.core.application.port.PaymentGateway;
import com.example.enterprisejava.frameworklab.core.domain.OrderId;

// Pattern: Saga / Process Manager - koordiniert mehrere fachliche Schritte ohne verteilte ACID-Transaktion.
public final class OrderFulfillmentSaga {
    private final OrderRepository repository;
    private final PaymentGateway paymentGateway;
    private final InventoryPort inventoryPort;
    private final InvoicePort invoicePort;
    private final OutboxPort outboxPort;

    public OrderFulfillmentSaga(OrderRepository repository, PaymentGateway paymentGateway,
                                InventoryPort inventoryPort, InvoicePort invoicePort, OutboxPort outboxPort) {
        this.repository = repository;
        this.paymentGateway = paymentGateway;
        this.inventoryPort = inventoryPort;
        this.invoicePort = invoicePort;
        this.outboxPort = outboxPort;
    }

    public OrderReceipt fulfill(OrderId orderId) {
        var order = repository.findById(orderId).orElseThrow(() -> new IllegalArgumentException("order not found: " + orderId));
        inventoryPort.reserve(order.lines());
        var authorization = paymentGateway.authorize(order.id(), order.total());
        order.authorizePayment(authorization);
        var invoiceNumber = invoicePort.createInvoice(order);
        order.createInvoice(invoiceNumber);
        order.fulfill();
        repository.save(order);
        order.pullEvents().forEach(outboxPort::append);
        return new OrderReceipt(order.id(), order.status(), order.total());
    }
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/application/OrderReceipt.java

java
package com.example.enterprisejava.frameworklab.core.application;

import com.example.enterprisejava.frameworklab.core.domain.Money;
import com.example.enterprisejava.frameworklab.core.domain.OrderId;
import com.example.enterprisejava.frameworklab.core.domain.OrderStatus;

public record OrderReceipt(OrderId orderId, OrderStatus status, Money total) {}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/application/PlaceOrderCommand.java

java
package com.example.enterprisejava.frameworklab.core.application;

import com.example.enterprisejava.frameworklab.core.domain.CustomerId;
import com.example.enterprisejava.frameworklab.core.domain.Money;
import java.util.List;

// Pattern: DTO / Command - eingehender Use-Case-Auftrag ohne Framework-Abhängigkeit.
public record PlaceOrderCommand(CustomerId customerId, List<Line> lines) {
    public record Line(String sku, int quantity, Money unitPrice) {}
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/application/PlaceOrderUseCase.java

java
package com.example.enterprisejava.frameworklab.core.application;

import com.example.enterprisejava.frameworklab.core.application.port.OrderRepository;
import com.example.enterprisejava.frameworklab.core.application.port.OutboxPort;
import com.example.enterprisejava.frameworklab.core.domain.Order;
import com.example.enterprisejava.frameworklab.core.domain.OrderId;
import com.example.enterprisejava.frameworklab.core.domain.OrderLine;

// Pattern: Application Service / Use Case - orchestriert, aber enthält keine HTTP-, DB- oder Framework-Details.
public final class PlaceOrderUseCase {
    private final OrderRepository repository;
    private final OutboxPort outbox;

    public PlaceOrderUseCase(OrderRepository repository, OutboxPort outbox) {
        this.repository = repository;
        this.outbox = outbox;
    }

    public OrderReceipt place(PlaceOrderCommand command) {
        var order = new Order(OrderId.newId(), command.customerId());
        command.lines().forEach(line -> order.addLine(new OrderLine(line.sku(), line.quantity(), line.unitPrice())));
        order.place();
        repository.save(order);
        order.pullEvents().forEach(outbox::append);
        return new OrderReceipt(order.id(), order.status(), order.total());
    }
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/application/port/InventoryPort.java

java
package com.example.enterprisejava.frameworklab.core.application.port;

import com.example.enterprisejava.frameworklab.core.domain.OrderLine;
import java.util.List;

public interface InventoryPort { void reserve(List<OrderLine> lines); }

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/application/port/InvoicePort.java

java
package com.example.enterprisejava.frameworklab.core.application.port;

import com.example.enterprisejava.frameworklab.core.domain.Order;

public interface InvoicePort { String createInvoice(Order order); }

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/application/port/OrderRepository.java

java
package com.example.enterprisejava.frameworklab.core.application.port;

import com.example.enterprisejava.frameworklab.core.domain.Order;
import com.example.enterprisejava.frameworklab.core.domain.OrderId;
import java.util.Optional;

// Pattern: Repository Port - Use Cases kennen Vertrag, nicht Datenbanktechnologie.
public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(OrderId id);
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/application/port/OutboxPort.java

java
package com.example.enterprisejava.frameworklab.core.application.port;

import com.example.enterprisejava.frameworklab.core.domain.DomainEvent;

// Pattern: Outbox Port - technische Event-Persistenz bleibt Adapter-Aufgabe.
public interface OutboxPort { void append(DomainEvent event); }

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/application/port/PaymentGateway.java

java
package com.example.enterprisejava.frameworklab.core.application.port;

import com.example.enterprisejava.frameworklab.core.domain.Money;
import com.example.enterprisejava.frameworklab.core.domain.OrderId;

// Pattern: Adapter Port - externe Payment-API wird durch fachlichen Vertrag entkoppelt.
public interface PaymentGateway { String authorize(OrderId orderId, Money amount); }

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/domain/CustomerId.java

java
package com.example.enterprisejava.frameworklab.core.domain;

import java.util.Objects;

// Pattern: Value Object - verhindert primitive obsession bei Kundennummern.
public record CustomerId(String value) {
    public CustomerId {
        Objects.requireNonNull(value, "value");
        if (value.isBlank()) throw new IllegalArgumentException("customer id must not be blank");
    }
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/domain/DomainEvent.java

java
package com.example.enterprisejava.frameworklab.core.domain;

import java.time.Instant;

// Pattern: Domain Event - beschreibt fachliche Tatsache statt technische Aktion.
public sealed interface DomainEvent permits OrderPlacedEvent, PaymentAuthorizedEvent, InvoiceCreatedEvent {
    OrderId orderId();
    Instant occurredAt();
    String eventType();
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/domain/InvoiceCreatedEvent.java

java
package com.example.enterprisejava.frameworklab.core.domain;

import java.time.Instant;

public record InvoiceCreatedEvent(OrderId orderId, Instant occurredAt, String invoiceNumber) implements DomainEvent {
    @Override public String eventType() { return "InvoiceCreated"; }
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/domain/Money.java

java
package com.example.enterprisejava.frameworklab.core.domain;

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

// Pattern: Value Object - zentrale Geldlogik statt BigDecimal-Streuung im Code.
public record Money(BigDecimal amount, Currency currency) implements Comparable<Money> {
    public Money {
        Objects.requireNonNull(amount, "amount");
        Objects.requireNonNull(currency, "currency");
        amount = amount.setScale(2, RoundingMode.HALF_UP);
    }
    public static Money eur(String amount) { return new Money(new BigDecimal(amount), Currency.getInstance("EUR")); }
    public Money add(Money other) { requireSameCurrency(other); return new Money(amount.add(other.amount), currency); }
    public Money multiply(int quantity) { return new Money(amount.multiply(BigDecimal.valueOf(quantity)), currency); }
    public boolean isPositive() { return amount.signum() > 0; }
    private void requireSameCurrency(Money other) {
        if (!currency.equals(other.currency)) throw new IllegalArgumentException("currency mismatch");
    }
    @Override public int compareTo(Money other) { requireSameCurrency(other); return amount.compareTo(other.amount); }
    @Override public String toString() { return amount + " " + currency.getCurrencyCode(); }
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/domain/Order.java

java
package com.example.enterprisejava.frameworklab.core.domain;

import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

// Pattern: Aggregate Root - Order schützt Statuswechsel, Invarianten und Domain Events.
public final class Order {
    private final OrderId id;
    private final CustomerId customerId;
    private final List<OrderLine> lines = new ArrayList<>();
    private final List<DomainEvent> events = new ArrayList<>();
    private OrderStatus status = OrderStatus.DRAFT;

    public Order(OrderId id, CustomerId customerId) {
        this.id = id;
        this.customerId = customerId;
    }
    public void addLine(OrderLine line) {
        ensureStatus(OrderStatus.DRAFT);
        lines.add(line);
    }
    public void place() {
        ensureStatus(OrderStatus.DRAFT);
        if (lines.isEmpty()) throw new IllegalStateException("order requires at least one line");
        status = OrderStatus.PLACED;
        events.add(new OrderPlacedEvent(id, Instant.now(), total()));
    }
    public void authorizePayment(String authorizationCode) {
        ensureStatus(OrderStatus.PLACED);
        status = OrderStatus.PAYMENT_AUTHORIZED;
        events.add(new PaymentAuthorizedEvent(id, Instant.now(), authorizationCode));
    }
    public void createInvoice(String invoiceNumber) {
        ensureStatus(OrderStatus.PAYMENT_AUTHORIZED);
        status = OrderStatus.INVOICE_CREATED;
        events.add(new InvoiceCreatedEvent(id, Instant.now(), invoiceNumber));
    }
    public void fulfill() { ensureStatus(OrderStatus.INVOICE_CREATED); status = OrderStatus.FULFILLED; }
    public Money total() { return lines.stream().map(OrderLine::lineTotal).reduce(Money.eur("0.00"), Money::add); }
    public List<DomainEvent> pullEvents() { 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 List<OrderLine> lines() { return Collections.unmodifiableList(lines); }
    private void ensureStatus(OrderStatus expected) {
        if (status != expected) throw new IllegalStateException("expected " + expected + " but was " + status);
    }
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/domain/OrderId.java

java
package com.example.enterprisejava.frameworklab.core.domain;

import java.util.Objects;
import java.util.UUID;

// Pattern: Value Object - kapselt technische UUID als fachliche Order-Identität.
public record OrderId(UUID value) {
    public OrderId { Objects.requireNonNull(value, "value"); }
    public static OrderId newId() { return new OrderId(UUID.randomUUID()); }
    @Override public String toString() { return value.toString(); }
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/domain/OrderLine.java

java
package com.example.enterprisejava.frameworklab.core.domain;

// Pattern: Value Object - Position ist unveränderlich und berechnet ihren Teilbetrag selbst.
public record OrderLine(String sku, int quantity, Money unitPrice) {
    public OrderLine {
        if (sku == null || sku.isBlank()) throw new IllegalArgumentException("sku required");
        if (quantity <= 0) throw new IllegalArgumentException("quantity must be positive");
        if (unitPrice == null || !unitPrice.isPositive()) throw new IllegalArgumentException("positive unit price required");
    }
    public Money lineTotal() { return unitPrice.multiply(quantity); }
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/domain/OrderPlacedEvent.java

java
package com.example.enterprisejava.frameworklab.core.domain;

import java.time.Instant;

public record OrderPlacedEvent(OrderId orderId, Instant occurredAt, Money total) implements DomainEvent {
    @Override public String eventType() { return "OrderPlaced"; }
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/domain/OrderStatus.java

java
package com.example.enterprisejava.frameworklab.core.domain;

public enum OrderStatus {
    DRAFT, PLACED, PAYMENT_AUTHORIZED, INVOICE_CREATED, FULFILLED, REJECTED
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/domain/PaymentAuthorizedEvent.java

java
package com.example.enterprisejava.frameworklab.core.domain;

import java.time.Instant;

public record PaymentAuthorizedEvent(OrderId orderId, Instant occurredAt, String authorizationCode) implements DomainEvent {
    @Override public String eventType() { return "PaymentAuthorized"; }
}

code/framework-labs/order-platform-core/src/main/java/com/example/enterprisejava/frameworklab/core/support/DemoApplication.java

java
package com.example.enterprisejava.frameworklab.core.support;

import com.example.enterprisejava.frameworklab.core.adapter.*;
import com.example.enterprisejava.frameworklab.core.application.*;
import com.example.enterprisejava.frameworklab.core.domain.*;
import java.util.List;

public final class DemoApplication {
    public static void main(String[] args) {
        var repository = new InMemoryOrderRepository();
        var outbox = new ConsoleOutboxAdapter();
        var placeOrder = new PlaceOrderUseCase(repository, outbox);
        var receipt = placeOrder.place(new PlaceOrderCommand(
                new CustomerId("C-1000"),
                List.of(new PlaceOrderCommand.Line("SKU-BOOK", 2, Money.eur("39.90")))));
        var saga = new OrderFulfillmentSaga(repository, new MockPaymentGateway(), new MockInventoryAdapter(), new MockInvoiceAdapter(), outbox);
        System.out.println(saga.fulfill(receipt.orderId()));
    }
}

code/framework-labs/pom.xml

xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example.enterprisejava</groupId>
  <artifactId>framework-labs-parent</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>pom</packaging>
  <modules>
    <module>order-platform-core</module>
    <module>spring-boot-order-api</module>
    <module>jakarta-ee-order-api</module>
    <module>quarkus-order-api</module>
    <module>micronaut-order-api</module>
  </modules>
  <properties>
    <maven.compiler.release>21</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <!-- Lehrbuch-Hinweis: Framework-Versionen sind bewusst zentralisiert. Im echten Projekt per Renovate/Dependabot aktualisieren. -->
    <spring.boot.version>3.3.5</spring.boot.version>
    <jakarta.ee.version>10.0.0</jakarta.ee.version>
    <quarkus.platform.version>3.15.1</quarkus.platform.version>
    <micronaut.version>4.6.3</micronaut.version>
  </properties>
</project>

code/framework-labs/quarkus-order-api/pom.xml

xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent><groupId>com.example.enterprisejava</groupId><artifactId>framework-labs-parent</artifactId><version>1.0.0-SNAPSHOT</version></parent>
  <artifactId>quarkus-order-api</artifactId>
  <dependencies>
    <dependency><groupId>com.example.enterprisejava</groupId><artifactId>order-platform-core</artifactId><version>${project.version}</version></dependency>
    <dependency><groupId>io.quarkus</groupId><artifactId>quarkus-rest</artifactId><version>${quarkus.platform.version}</version></dependency>
    <dependency><groupId>io.quarkus</groupId><artifactId>quarkus-arc</artifactId><version>${quarkus.platform.version}</version></dependency>
  </dependencies>
</project>

code/framework-labs/quarkus-order-api/src/main/java/com/example/enterprisejava/frameworklab/quarkus/api/OrderResource.java

java
package com.example.enterprisejava.frameworklab.quarkus.api;

import com.example.enterprisejava.frameworklab.core.application.PlaceOrderCommand;
import com.example.enterprisejava.frameworklab.core.application.PlaceOrderUseCase;
import com.example.enterprisejava.frameworklab.core.domain.CustomerId;
import com.example.enterprisejava.frameworklab.core.domain.Money;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;

// Pattern: Thin Resource - Quarkus-spezifisch, fachlich aber identisch zum Core.
@Path("/orders")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class OrderResource {
    @Inject PlaceOrderUseCase placeOrderUseCase;
    @POST
    public Response place(OrderRequest request) {
        var command = new PlaceOrderCommand(new CustomerId(request.customerId()),
                request.lines().stream().map(line -> new PlaceOrderCommand.Line(line.sku(), line.quantity(), Money.eur(line.unitPrice()))).toList());
        var receipt = placeOrderUseCase.place(command);
        return Response.status(201).entity(new OrderResponse(receipt.orderId().toString(), receipt.status().name(), receipt.total().toString())).build();
    }
    public record OrderRequest(String customerId, List<LineRequest> lines) {}
    public record LineRequest(String sku, int quantity, String unitPrice) {}
    public record OrderResponse(String orderId, String status, String total) {}
}

code/framework-labs/quarkus-order-api/src/main/java/com/example/enterprisejava/frameworklab/quarkus/config/CoreProducer.java

java
package com.example.enterprisejava.frameworklab.quarkus.config;

import com.example.enterprisejava.frameworklab.core.adapter.ConsoleOutboxAdapter;
import com.example.enterprisejava.frameworklab.core.adapter.InMemoryOrderRepository;
import com.example.enterprisejava.frameworklab.core.application.PlaceOrderUseCase;
import com.example.enterprisejava.frameworklab.core.application.port.OrderRepository;
import com.example.enterprisejava.frameworklab.core.application.port.OutboxPort;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;

// Pattern: Composition Root - Quarkus Arc erzeugt Core-Beans ohne Core-Abhängigkeit zu Quarkus.
@ApplicationScoped
public class CoreProducer {
    @Produces @ApplicationScoped OrderRepository orderRepository() { return new InMemoryOrderRepository(); }
    @Produces @ApplicationScoped OutboxPort outboxPort() { return new ConsoleOutboxAdapter(); }
    @Produces @ApplicationScoped PlaceOrderUseCase placeOrderUseCase(OrderRepository repo, OutboxPort outbox) { return new PlaceOrderUseCase(repo, outbox); }
}

code/framework-labs/spring-boot-order-api/pom.xml

xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent><groupId>com.example.enterprisejava</groupId><artifactId>framework-labs-parent</artifactId><version>1.0.0-SNAPSHOT</version></parent>
  <artifactId>spring-boot-order-api</artifactId>
  <dependencies>
    <dependency><groupId>com.example.enterprisejava</groupId><artifactId>order-platform-core</artifactId><version>${project.version}</version></dependency>
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>${spring.boot.version}</version></dependency>
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId><version>${spring.boot.version}</version></dependency>
  </dependencies>
</project>

code/framework-labs/spring-boot-order-api/src/main/java/com/example/enterprisejava/frameworklab/spring/OrderSpringApplication.java

java
package com.example.enterprisejava.frameworklab.spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class OrderSpringApplication {
    public static void main(String[] args) { SpringApplication.run(OrderSpringApplication.class, args); }
}

code/framework-labs/spring-boot-order-api/src/main/java/com/example/enterprisejava/frameworklab/spring/api/OrderController.java

java
package com.example.enterprisejava.frameworklab.spring.api;

import com.example.enterprisejava.frameworklab.core.application.PlaceOrderCommand;
import com.example.enterprisejava.frameworklab.core.application.PlaceOrderUseCase;
import com.example.enterprisejava.frameworklab.core.domain.CustomerId;
import com.example.enterprisejava.frameworklab.core.domain.Money;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;

// Pattern: Thin Controller - HTTP bleibt Adapter, Fachlogik bleibt im Use Case.
@RestController
@RequestMapping("/orders")
public class OrderController {
    private final PlaceOrderUseCase placeOrderUseCase;
    public OrderController(PlaceOrderUseCase placeOrderUseCase) { this.placeOrderUseCase = placeOrderUseCase; }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public OrderResponse place(@Valid @RequestBody OrderRequest request) {
        var command = new PlaceOrderCommand(new CustomerId(request.customerId()),
                request.lines().stream().map(line -> new PlaceOrderCommand.Line(line.sku(), line.quantity(), Money.eur(line.unitPrice()))).toList());
        var receipt = placeOrderUseCase.place(command);
        return new OrderResponse(receipt.orderId().toString(), receipt.status().name(), receipt.total().toString());
    }
    public record OrderRequest(@NotBlank String customerId, List<LineRequest> lines) {}
    public record LineRequest(@NotBlank String sku, @Min(1) int quantity, @NotBlank String unitPrice) {}
    public record OrderResponse(String orderId, String status, String total) {}
}

code/framework-labs/spring-boot-order-api/src/main/java/com/example/enterprisejava/frameworklab/spring/config/OrderBeansConfig.java

java
package com.example.enterprisejava.frameworklab.spring.config;

import com.example.enterprisejava.frameworklab.core.adapter.ConsoleOutboxAdapter;
import com.example.enterprisejava.frameworklab.core.adapter.InMemoryOrderRepository;
import com.example.enterprisejava.frameworklab.core.application.PlaceOrderUseCase;
import com.example.enterprisejava.frameworklab.core.application.port.OrderRepository;
import com.example.enterprisejava.frameworklab.core.application.port.OutboxPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// Pattern: Composition Root - Framework-spezifisches Wiring bleibt zentral im Adapterprojekt.
@Configuration
public class OrderBeansConfig {
    @Bean OrderRepository orderRepository() { return new InMemoryOrderRepository(); }
    @Bean OutboxPort outboxPort() { return new ConsoleOutboxAdapter(); }
    @Bean PlaceOrderUseCase placeOrderUseCase(OrderRepository repo, OutboxPort outbox) { return new PlaceOrderUseCase(repo, outbox); }
}

code/framework-labs/spring-boot-order-api/src/main/java/com/example/enterprisejava/frameworklab/spring/error/RestExceptionAdvice.java

java
package com.example.enterprisejava.frameworklab.spring.error;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.time.Instant;

// Pattern: Error Mapper - technische Exceptions werden in stabile API-Fehler übersetzt.
@RestControllerAdvice
public class RestExceptionAdvice {
    @ExceptionHandler({IllegalArgumentException.class, IllegalStateException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiError business(RuntimeException ex) { return new ApiError("BUSINESS_RULE_VIOLATION", ex.getMessage(), Instant.now()); }
    public record ApiError(String code, String message, Instant timestamp) {}
}

code/runtime-modernization-lab/pom.xml

xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example.enterprisejava</groupId>
  <artifactId>runtime-modernization-lab</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <properties>
    <java.release>21</java.release>
    <spring.boot.version>4.1.0</spring.boot.version>
    <jakarta.ee.version>11.0.0</jakarta.ee.version>
    <quarkus.platform.version>3.33.1.1</quarkus.platform.version>
    <micronaut.java21.track>4.x</micronaut.java21.track>
    <micronaut.java25.track>5.0.0</micronaut.java25.track>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.14.0</version>
        <configuration>
          <release>${java.release}</release>
        </configuration>
      </plugin>
      <!-- Pattern: Build Gate. Im echten Projekt maven-enforcer-plugin mit Java-, Dependency- und Repository-Regeln aktivieren. -->
      <!-- Pattern: Supply Chain Evidence. Im echten Projekt CycloneDX, Lizenzbericht und CVE-Check in CI erzwingen. -->
    </plugins>
  </build>

  <profiles>
    <profile>
      <id>java21-core</id>
      <activation><activeByDefault>true</activeByDefault></activation>
      <!-- Core bleibt JDK-only und frameworkfrei. -->
    </profile>
    <profile>
      <id>spring-boot-4-track</id>
      <!-- Spring Boot 4.x Track: Java 17+, im Buch mit Java 21 betreiben. -->
    </profile>
    <profile>
      <id>jakarta-ee-11-track</id>
      <!-- Jakarta EE 11 Track: Application-Server-/WAR-Modernisierung. -->
    </profile>
    <profile>
      <id>quarkus-333-lts-track</id>
      <!-- Quarkus 3.33 LTS Track: Container-first Produktionsspur. -->
    </profile>
    <profile>
      <id>micronaut-5-java25-track</id>
      <!-- Java-25-Zukunftsspur, nicht Teil des Java-21-Core-Builds. -->
    </profile>
  </profiles>
</project>

code/runtime-modernization-lab/src/main/java/com/example/enterprisejava/runtime2026/ApplicationInventory.java

java
package com.example.enterprisejava.runtime2026;

import java.util.List;

// Pattern: Immutable Value Object - Inventarzustand ist reproduzierbar und testbar.
public record ApplicationInventory(
        String name,
        int currentJava,
        RuntimeLine targetRuntime,
        boolean usesApplicationServer,
        boolean usesSpringEcosystem,
        boolean containerFirst,
        List<String> criticalDependencies
) {
    public ApplicationInventory {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("name required");
        criticalDependencies = List.copyOf(criticalDependencies == null ? List.of() : criticalDependencies);
    }
}

code/runtime-modernization-lab/src/main/java/com/example/enterprisejava/runtime2026/DefaultRiskClassifier.java

java
package com.example.enterprisejava.runtime2026;

// Pattern: Strategy Implementation - Standardlogik für Lernprojekt und CI-Prototyp.
public final class DefaultRiskClassifier implements RiskClassifier {
    @Override
    public RiskLevel classify(ApplicationInventory inventory, RuntimePolicy policy) {
        if (inventory.currentJava() < policy.recommendedJavaBaseline()) {
            return policy.java25Track() ? RiskLevel.BLOCKED : RiskLevel.HIGH;
        }
        if (inventory.criticalDependencies().size() > 8) return RiskLevel.HIGH;
        if (inventory.usesApplicationServer() && policy.line() == RuntimeLine.SPRING_BOOT_4) return RiskLevel.MEDIUM;
        if (policy.line() == RuntimeLine.MICRONAUT_5_JAVA25) return RiskLevel.BLOCKED;
        return RiskLevel.LOW;
    }
}

code/runtime-modernization-lab/src/main/java/com/example/enterprisejava/runtime2026/DemoApplication.java

java
package com.example.enterprisejava.runtime2026;

import java.util.List;

public final class DemoApplication {
    public static void main(String[] args) {
        var inventory = new ApplicationInventory(
                "Order API",
                21,
                RuntimeLine.SPRING_BOOT_4,
                false,
                true,
                true,
                List.of("spring-security", "jpa", "postgresql", "kafka", "micrometer"));

        var planner = RuntimeDecisionFactory.defaultPlanner();
        System.out.println(planner.planFor(inventory).asMarkdown());
        System.out.println("Governance checks:");
        DependencyGovernanceChecklist.enterpriseDefault().checks().forEach(check -> System.out.println("- " + check));
    }
}

code/runtime-modernization-lab/src/main/java/com/example/enterprisejava/runtime2026/DependencyGovernanceChecklist.java

java
package com.example.enterprisejava.runtime2026;

import java.util.List;

// Pattern: Value Object - Checkliste ist ein expliziter Liefergegenstand, nicht Chat-Text.
public record DependencyGovernanceChecklist(List<String> checks) {
    public static DependencyGovernanceChecklist enterpriseDefault() {
        return new DependencyGovernanceChecklist(List.of(
                "Zentrale Version Properties oder Version Catalog",
                "BOM Import je Framework",
                "maven-enforcer-plugin fuer Java- und Dependency-Regeln",
                "SBOM mit CycloneDX erzeugen",
                "CVE-Scan in CI bewerten",
                "Lizenzbericht erzeugen",
                "Rollback-Artefakt dokumentieren"));
    }
}

code/runtime-modernization-lab/src/main/java/com/example/enterprisejava/runtime2026/InMemoryRuntimePolicyRepository.java

java
package com.example.enterprisejava.runtime2026;

import java.util.*;

// Pattern: In-Memory Repository - Demo-Implementierung ohne Datenbank oder externe Services.
public final class InMemoryRuntimePolicyRepository implements RuntimePolicyRepository {
    private final Map<RuntimeLine, RuntimePolicy> policies = new EnumMap<>(RuntimeLine.class);

    public InMemoryRuntimePolicyRepository() {
        add(new RuntimePolicy(RuntimeLine.JAVA21_CORE, 21, "21", List.of("javac --release 21", "Core ohne Framework-Imports"), false));
        add(new RuntimePolicy(RuntimeLine.SPRING_BOOT_4, 21, "4.1.0", List.of("Spring Framework 7.x", "Servlet 6.1", "Security Tests", "Actuator Check"), false));
        add(new RuntimePolicy(RuntimeLine.JAKARTA_EE_11, 21, "11", List.of("Server-Kompatibilität", "JTA/JPA Tests", "WAR Deployment"), false));
        add(new RuntimePolicy(RuntimeLine.QUARKUS_333_LTS, 21, "3.33 LTS", List.of("Container Startzeit", "Health Check", "Native optional"), false));
        add(new RuntimePolicy(RuntimeLine.MICRONAUT_4_JAVA21, 21, "4.x", List.of("Compile-time DI", "HTTP Tests", "Config Tests"), false));
        add(new RuntimePolicy(RuntimeLine.MICRONAUT_5_JAVA25, 25, "5.0.0", List.of("Java 25 Toolchain", "separater Release Train", "Team-Freigabe"), true));
    }

    private void add(RuntimePolicy policy) { policies.put(policy.line(), policy); }
    @Override public Optional<RuntimePolicy> find(RuntimeLine line) { return Optional.ofNullable(policies.get(line)); }
    @Override public List<RuntimePolicy> findAll() { return List.copyOf(policies.values()); }
}

code/runtime-modernization-lab/src/main/java/com/example/enterprisejava/runtime2026/RiskClassifier.java

java
package com.example.enterprisejava.runtime2026;

// Pattern: Strategy - Risikobewertung kann je Organisation ausgetauscht werden.
public interface RiskClassifier {
    RiskLevel classify(ApplicationInventory inventory, RuntimePolicy policy);
}

code/runtime-modernization-lab/src/main/java/com/example/enterprisejava/runtime2026/RiskLevel.java

java
package com.example.enterprisejava.runtime2026;

public enum RiskLevel { LOW, MEDIUM, HIGH, BLOCKED }

code/runtime-modernization-lab/src/main/java/com/example/enterprisejava/runtime2026/RuntimeDecisionFactory.java

java
package com.example.enterprisejava.runtime2026;

// Pattern: Factory - Demo verdrahtet Standard-Implementierungen an einer Stelle.
public final class RuntimeDecisionFactory {
    private RuntimeDecisionFactory() {}

    public static UpgradePlanner defaultPlanner() {
        return new UpgradePlanner(new InMemoryRuntimePolicyRepository(), new DefaultRiskClassifier());
    }
}

code/runtime-modernization-lab/src/main/java/com/example/enterprisejava/runtime2026/RuntimeLine.java

java
package com.example.enterprisejava.runtime2026;

// Pattern: Type-Safe Enum - Runtime-Entscheidungen werden nicht als freie Strings verteilt.
public enum RuntimeLine {
    JAVA21_CORE("Java 21 Core", 21, "fachlicher Kern ohne Framework-Abhängigkeiten"),
    SPRING_BOOT_4("Spring Boot 4.x", 17, "Spring Framework 7.x und Jakarta EE 11 Baseline"),
    JAKARTA_EE_11("Jakarta EE 11", 17, "Standardplattform für Enterprise-Container"),
    QUARKUS_333_LTS("Quarkus 3.33 LTS", 21, "containernahe LTS-Spur"),
    MICRONAUT_4_JAVA21("Micronaut 4.x", 21, "Java-21-kompatibles Vergleichslab"),
    MICRONAUT_5_JAVA25("Micronaut 5.x", 25, "separate Java-25-Zukunftsspur");

    private final String label;
    private final int minimumJava;
    private final String purpose;

    RuntimeLine(String label, int minimumJava, String purpose) {
        this.label = label;
        this.minimumJava = minimumJava;
        this.purpose = purpose;
    }

    public String label() { return label; }
    public int minimumJava() { return minimumJava; }
    public String purpose() { return purpose; }
}

code/runtime-modernization-lab/src/main/java/com/example/enterprisejava/runtime2026/RuntimePolicy.java

java
package com.example.enterprisejava.runtime2026;

import java.util.List;

// Pattern: Immutable Policy Object - Regeln sind explizit und können versioniert werden.
public record RuntimePolicy(
        RuntimeLine line,
        int recommendedJavaBaseline,
        String recommendedVersion,
        List<String> requiredChecks,
        boolean java25Track
) {
    public RuntimePolicy {
        requiredChecks = List.copyOf(requiredChecks == null ? List.of() : requiredChecks);
    }
}

code/runtime-modernization-lab/src/main/java/com/example/enterprisejava/runtime2026/RuntimePolicyRepository.java

java
package com.example.enterprisejava.runtime2026;

import java.util.*;

// Pattern: Repository - Zugriff auf Runtime-Regeln ist gekapselt und austauschbar.
public interface RuntimePolicyRepository {
    Optional<RuntimePolicy> find(RuntimeLine line);
    List<RuntimePolicy> findAll();
}

code/runtime-modernization-lab/src/main/java/com/example/enterprisejava/runtime2026/UpgradePlan.java

java
package com.example.enterprisejava.runtime2026;

import java.util.*;

// Pattern: Builder - komplexer Plan wird lesbar und unveränderlich aufgebaut.
public final class UpgradePlan {
    private final String application;
    private final RuntimeLine target;
    private final int javaBaseline;
    private final RiskLevel risk;
    private final List<String> steps;

    private UpgradePlan(Builder builder) {
        this.application = builder.application;
        this.target = builder.target;
        this.javaBaseline = builder.javaBaseline;
        this.risk = builder.risk;
        this.steps = List.copyOf(builder.steps);
    }

    public static Builder builder(String application, RuntimeLine target) { return new Builder(application, target); }
    public String application() { return application; }
    public RuntimeLine target() { return target; }
    public int javaBaseline() { return javaBaseline; }
    public RiskLevel risk() { return risk; }
    public List<String> steps() { return steps; }

    public String asMarkdown() {
        var out = new StringBuilder();
        out.append("## Upgrade Plan: ").append(application).append("\n");
        out.append("- Target: ").append(target.label()).append("\n");
        out.append("- Java baseline: ").append(javaBaseline).append("\n");
        out.append("- Risk: ").append(risk).append("\n\n");
        for (int i = 0; i < steps.size(); i++) out.append(i + 1).append(". ").append(steps.get(i)).append("\n");
        return out.toString();
    }

    public static final class Builder {
        private final String application;
        private final RuntimeLine target;
        private int javaBaseline = 21;
        private RiskLevel risk = RiskLevel.MEDIUM;
        private final List<String> steps = new ArrayList<>();

        private Builder(String application, RuntimeLine target) { this.application = application; this.target = target; }
        public Builder javaBaseline(int javaBaseline) { this.javaBaseline = javaBaseline; return this; }
        public Builder risk(RiskLevel risk) { this.risk = risk; return this; }
        public Builder addStep(String step) { this.steps.add(step); return this; }
        public UpgradePlan build() { return new UpgradePlan(this); }
    }
}

code/runtime-modernization-lab/src/main/java/com/example/enterprisejava/runtime2026/UpgradePlanner.java

java
package com.example.enterprisejava.runtime2026;

// Pattern: Application Service / Facade - ein einfacher Einstiegspunkt für die Modernisierungsentscheidung.
public final class UpgradePlanner {
    private final RuntimePolicyRepository policies;
    private final RiskClassifier riskClassifier;

    public UpgradePlanner(RuntimePolicyRepository policies, RiskClassifier riskClassifier) {
        this.policies = policies;
        this.riskClassifier = riskClassifier;
    }

    public UpgradePlan planFor(ApplicationInventory inventory) {
        RuntimePolicy policy = policies.find(inventory.targetRuntime())
                .orElseThrow(() -> new IllegalArgumentException("No policy for " + inventory.targetRuntime()));
        RiskLevel risk = riskClassifier.classify(inventory, policy);
        return UpgradePlan.builder(inventory.name(), inventory.targetRuntime())
                .javaBaseline(policy.recommendedJavaBaseline())
                .risk(risk)
                .addStep("Core mit javac --release 21 pruefen")
                .addStep("Framework-Adapter getrennt vom Core aktualisieren")
                .addStep("BOMs und Maven Enforcer Regeln aktivieren")
                .addStep("SBOM, CVE-Scan und Lizenzbericht erzeugen")
                .addStep("HTTP-, DI-, Security- und Betriebschecks ausfuehren")
                .addStep("Rollback-Plan und ADR dokumentieren")
                .build();
    }
}
↑ Anfang ⌂ Cockpit