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.
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.pdfnutzen.
Inhaltliche Landkarte
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.
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.
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.
Ü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
Die finale Prüfung umfasst:
- lokale HTML-Linkprüfung ohne externe Links,
- Vorhandensein der zentralen Startdateien,
- Renderprobe des finalen Portal-PDFs,
javac --release 21fü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
// 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);
}
}
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
[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:
- Kannst du den Unterschied zwischen Java 11, 17 und 21 im Enterprise-Kontext erklären?
- Kannst du begründen, warum der Core frameworkfrei bleiben sollte?
- Kannst du den Outbox-Flow fachlich und technisch erklären?
- Kannst du sagen, wo Transaktionen beginnen und enden sollten?
- Kannst du mindestens fünf verwendete Entwurfsmuster im Code zeigen?
- Kannst du erklären, warum Framework-Adapter separat geprüft werden?
- 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
Zielbild des Buchs
Das Buch beantwortet vier zentrale Fragen:
- Welche Java-Features aus 11, 17 und 21 sind für Enterprise-Systeme wirklich relevant?
- Wie entwirft man wartbare Architekturen mit klaren Grenzen zwischen API, Anwendung, Domäne und Infrastruktur?
- Wie sehen komplexere Codebeispiele aus, die nicht nur Spielzeug sind?
- 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 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 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 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
// 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
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.
// 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
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
// 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:
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:
// 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.
// Pattern: Repository Pattern.
// Zweck: fachliche Persistenzschnittstelle statt verstreuter EntityManager-Zugriffe.
public interface OrderRepository {
Optional<Order> findById(OrderId id);
void save(Order order);
}
// 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.
{
"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
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.
@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
Eine seriöse Migration sollte in Schritten erfolgen:
- Build reproduzierbar machen.
- Tests und Smoke Checks ergänzen.
- Dependencies aktualisieren.
- Java 11 als technische Baseline herstellen.
- Java 17 für moderne Sprache und LTS-Basis einsetzen.
- 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
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
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
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
// Vorher: technische und fachliche Logik vermischt.
if (status.equals("P") && amount.compareTo(BigDecimal.ZERO) > 0 && customer != null) {
// payment, invoice, mail, db update
}
// 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
// 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.
// 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 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
// 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
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 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.
Client
-> REST Controller
-> Application Service / Use Case
-> Domain Model
-> Ports
-> Adapter: JPA, HTTP, Kafka
Hexagonale Architektur
Ports und Adapter schützen den fachlichen Kern
Beispielhafte Paketstruktur
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
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
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
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.
Parent-POM-Regel
<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.
Ausgangsproblem
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
// 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
- Tests um aktuelles Verhalten legen.
- reine Berechnungen extrahieren.
- Ports für externe Systeme einführen.
- fachliche Ergebnisse explizit modellieren.
- Datenbanktransaktion und Outbox klären.
- Controller/API getrennt anpassen.
- 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
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
// 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
@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
// 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
public record CreateOrderRequest(
String customerNumber,
List<CreateOrderLineRequest> lines,
String paymentMethod
) {}
public record CreateOrderResponse(
UUID orderId,
String status,
List<LinkDto> links
) {}
Fehlervertrag
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
// 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
Event Design
public record OrderPlacedEvent(
UUID eventId,
Instant occurredAt,
UUID orderId,
String customerNumber,
BigDecimal totalAmount,
String currency
) implements DomainEvent {}
Idempotenter Consumer
// 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
Domain Test
@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
// 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
- Metriken prüfen: Latenz, Durchsatz, Fehler, Sättigung.
- Logs mit Korrelation auswerten.
- Datenbankqueries messen.
- Thread Dumps und Flight Recorder nutzen.
- Erst dann Code optimieren.
Threadpool vs Virtual Threads
// 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.
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
// 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.
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
// 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 |
| 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.
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.
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.
<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.
@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.
@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.
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.
// 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 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.
// 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.
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.
// 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");
}
// 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.
Das zentrale Lernprinzip lautet:
Der Enterprise-Core gehört nicht einem Framework. Frameworks sind Adapter, Runtimes und Integrationsumgebungen.
Paketstruktur der Framework-Labs
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/
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.
// 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.
@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.
@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.
@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.
@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
| 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
Die Teststrategie sollte nicht viermal dieselbe fachliche Logik testen. Besser:
- Core Unit Tests prüfen Businessregeln.
- Adapter Tests prüfen Mapping, HTTP-Status, Validation und Fehlerformat.
- Integration Tests prüfen Runtime-Wiring.
- Contract Tests sichern API-Verträge.
- Architekturtests prüfen, dass der Core keine Framework-Pakete importiert.
// 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
| 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.
<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.
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.
Gute Schichtenregel
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
// 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.
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
@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
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.
@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.
@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.
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
order-platform-core/docs/design-patterns.htmlorder-platform-core/src/main/java/.../domain/Order.javaorder-platform-core/src/main/java/.../application/PlaceOrderUseCase.javaorder-platform-core/src/main/java/.../application/OrderFulfillmentSaga.java- 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
+--------------------+ +---------------------+
| 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.
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.
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:
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
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.
// 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
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.
Spring Boot 4.x und 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:
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:
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:
<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
| 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:
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
Der Java-21-Core ist kein Spielball der Framework-Versionen.
Framework-Adapter dürfen sich ändern, aber die Fachlogik bleibt stabil.
Modernisierungssequenz
- Anwendung inventarisieren: Java-Version, Framework, Packaging, Datenbank, Messaging, Security.
- Fachlichen Core identifizieren und Framework-Imports entfernen.
- Java-21-Kompilierung mit
--release 21erzwingen. - Runtime-Ziel wählen: Spring Boot 4.x, Jakarta EE 11, Quarkus 3.33 LTS, Micronaut 4.x oder Java-25-Spur.
- BOMs zentralisieren.
- Maven Enforcer Regeln aktivieren.
- SBOM und Lizenzbericht erzeugen.
- Integrationstests gegen echte Adapter laufen lassen.
- Betriebsnachweise prüfen: Health, Metrics, Logs, Rollback.
- Ergebnis im Architekturentscheid dokumentieren.
Beispiel: ADR für Runtime-Entscheidung
# 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.
Was sich fachlich nicht ändern sollte
Die folgenden Klassen sollten nicht wissen, ob Spring Boot 4 oder Jakarta EE 11 verwendet wird:
Order
OrderLine
Money
PlaceOrderUseCase
PaymentPort
OutboxPort
OrderRepository
Diese Klassen gehören in den Core.
Was sich ändern darf
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
// Kein Spring, kein Jakarta, kein Quarkus, kein Micronaut im Core.
public interface PaymentPort {
PaymentDecision authorize(PaymentRequest request);
}
Beispiel: Spring Adapter
// 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
// 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.
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:
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
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
<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:
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
<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
<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
<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
<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
<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.
Welches Projekt beantwortet welche Frage?
| Projekt | Leitfrage | Einstieg |
|---|---|---|
enterprise-java-platform-lab | Wie werden Domäne, Use Case, Ports und Adapter ohne Framework sichtbar? | platform/support/DemoApplication.java |
enterprise-order-billing-demo | Wie läuft eine Bestellung durch Payment, Billing und Outbox? | orderbilling/DemoApplication.java |
order-platform-core | Welche Teile bleiben bei einem Frameworkwechsel stabil? | core/support/DemoApplication.java |
| Framework-Adapter | Wie wird HTTP in denselben Use Case übersetzt? | OrderController oder OrderResource |
runtime-modernization-lab | Wie entsteht aus Inventar, Zielruntime und Policy ein Upgradeplan? | runtime2026/DemoApplication.java |
Empfohlene Leserichtung
- Bei
DemoApplication,OrderControlleroderOrderResourcebeginnen. - Den aufgerufenen Use Case und nur dessen Orchestrierung lesen.
- Domänenmethoden mit Invarianten und Zustandswechseln verfolgen.
- Die verwendeten Port-Interfaces notieren.
- Zu jedem Port genau einen Adapter ansehen.
- 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
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
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
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
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
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
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
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
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
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
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
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
<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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
<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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
<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
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
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
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
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
<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
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
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
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
<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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
<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
<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
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
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
<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
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
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
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
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
<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
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
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
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
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
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
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
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
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
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
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
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
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
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();
}
}