Modul A

🧭 Orientierung, Lernarchitektur & Verzeichnis

Start, Bedienung, Roadmap, Lernpfade, Strukturentscheidungen und PDF/Print-Strategie.

10Themen in diesem Modul
0Codeblöcke
6SVG-Grafiken

Java Master-Handbuch: Modulstruktur Edition · seb4u Package Standard

Die komplette Java-Master-Edition ist jetzt thematisch nach Modulen gruppiert: Sprache, Runtime, Professional Backend, CommerceFlow, Enterprise, Fullstack, Cloud, Algorithmen und Job-Ready.

All-in-One HTMLInline SVG, keine externen BilderSidebar + SucheLedger + Code-AtlasDruckfreundlichProfessional Deep Dives + CommerceFlow Master-Projekt
🧭
Struktur angenommen · Finale Lernarchitektur

Akzeptierte Gesamtstruktur: vom Java-Core bis zur Senior-/Projekt-Reife

Modul A · Orientierung, Lernarchitektur & Verzeichnis

Diese Version setzt den Strukturvorschlag als verbindliche Ordnung um: stabile Kapitelnummern, klare Lernpfade, separate Vertiefungen und eine saubere Trennung zwischen Grundlagen, Professional Java, Enterprise, Fullstack, Cloud und Interview-Mastery.

Prinzip der neuen Struktur

Das Handbuch bleibt linear lesbar, wird aber in Lernmodule gegliedert. Dadurch kannst du es als Kurs, Nachschlagewerk, Projektbegleitung oder Interview-Vorbereitung verwenden. Bestehende Kapitelnummern bleiben stabil, neue Vertiefungen werden hinten ergänzt und über Pfade miteinander verbunden.

Master-Regel: Nicht jedes Kapitel muss sofort vollständig gelernt werden. Erst Core verstehen, dann mit Code-Labs anwenden, danach über CommerceFlow und Enterprise-Themen produktionsreif machen.
Finale Lernarchitektur nach Modulen Die Hauptnavigation folgt Themenbereichen A-K; alte K-Nummern dienen nur als kleine Referenz. Modul AOrientierung Modul BCore Java Modul CRuntime Modul DBackend Modul EProjekt Modul FEnterprise Modul GJob Ready Modul HFullstack Modul ICloud Modul JAlgorithmen Modul KQuellen und Anhang

Modulgruppen

Modul A · Orientierung, Lernarchitektur & VerzeichnisStart, Bedienung, Roadmap, Lernpfade, Strukturentscheidungen und PDF/Print-Strategie.
Themenstart
Modul B · Core Java, Sprache & TypsystemJDK, Syntax, OOP, Records, Collections, Generics, Streams und Fehlerverträge.
Core
Modul C · Runtime, I/O, Zeit & ConcurrencyDateien, NIO.2, Ressourcen, Zeit/Locale, Virtual Threads, Async, GC, Module und Build.
Runtime
Modul D · Professional Backend, Architektur & Code-LabsTesting, HTTP, JDBC, Security, Patterns, DDD, Performance, Observability und Refactoring.
Professional
Modul E · CommerceFlow Master-ProjektDurchgehendes Projekt mit Domain, Application Layer, HTTP/JSON, Outbox, Worker und Tests.
Projekt
Modul F · Enterprise JavaSpring, Persistence, Testing, Security, Production Readiness und Enterprise-Runbook.
Enterprise
Modul G · Job-Ready, Code Review & InterviewAufgaben, Lösungen, Review-Fallen, Senior-Fragen, Architektur-Cases und Portfolio.
Karriere
Modul H · Fullstack Java EditionREST API, Thymeleaf, Frontend-Integration, Login/Register, Upload, Export und UX.
Fullstack
Modul I · Microservices, Messaging & CloudGateway, Messaging, Outbox, Saga, Resilience, Docker, Kubernetes und Observability.
Cloud
Modul J · Algorithmen & Datenstruktur MasteryBig-O, HashMap, Sliding Window, Graphen, DP, Backtracking, Trie und Interview-Patterns.
Algorithmen
Modul K · Quellen, Versionshinweise & AnhangQuellen, Versionshinweise, Qualitätscheck und ergänzende Hinweise.
Anhang

Empfohlene Lernpfade

Pfad A · Core → Professional

Für solides Java-Verständnis ohne Framework-Abhängigkeit.

Core JavaProfessional JavaCode-Atlas

Pfad B · Backend Enterprise

Für REST, Datenbank, Spring Boot, Security und Production.

HTTPJDBCEnterprise

Pfad C · CommerceFlow Projekt

Für Portfolio, Architektur und sauberes End-to-End-Denken.

CommerceFlowEnterprise UpgradePortfolio

Pfad D · Senior & Cloud

Für Systemdesign, Microservices, Resilience und Betrieb.

89–96ObservabilityRelease

Pfad E · Interview Mastery

Für Aufgaben, Musterantworten, Datenstrukturen und Code-Review.

Job ReadyAlgorithmenReview

Pfad F · Fullstack Java

Für REST API, UI, Login, Datei-Upload und Reporting.

FullstackRESTUpload/Export

Struktur-Entscheidungen

EntscheidungBegründungNutzen
Kapitelnummern stabil haltenNeue Themen werden ergänzt, nicht mitten im Buch eingeschoben.Interne Links, Lernnotizen und Wiederholungen bleiben gültig.
Deep Dives nach HauptkapitelnGrundlagen bleiben lesbar, Profiwissen bekommt eigene Tiefe.Einsteiger werden nicht überladen, Fortgeschrittene bekommen Substanz.
CommerceFlow als roter FadenArchitektur, I/O, Concurrency, Security und Testing werden an einem Projekt verbunden.Das Handbuch wirkt wie ein echter Masterkurs statt wie eine lose Artikelsammlung.
Job-/Interview-Teil getrenntAufgaben, Musterantworten und Portfolio benötigen einen anderen Lesemodus.Du kannst gezielt für Bewerbung, Gespräch und Code-Review üben.
📦

Package-Standard: com.seb4u.demo

Modul A · Orientierung, Lernarchitektur & Verzeichnis

Alle eigenen Java-Beispiele verwenden ab dieser Version ein einheitliches Package-Schema: com.seb4u.demo, com.seb4u.demo.spring, com.seb4u.demo.jakarta oder bewusst kein Package bei Mini-Snippets.

KontextPackage-RegelBeispiel
Frameworkfrei / Core Javacom.seb4u.demo[.thema]package com.seb4u.demo.domain;
Spring Boot / Spring MVC / Spring Data / Spring Securitycom.seb4u.demo.spring[.thema]package com.seb4u.demo.spring.commerceflow.orders.api;
Jakarta EE / Jakarta Servlet / Jakarta Persistence ohne Springcom.seb4u.demo.jakarta[.thema]package com.seb4u.demo.jakarta.web;
Sehr kleine Syntax-Snippetskein Packagez. B. einzelne Methoden, einzelne Ausdrücke oder Interview-Mini-Aufgaben.
Master-Regel: Eigene Projektpakete beginnen immer mit com.seb4u.demo; Framework-spezifische Beispiele verwenden com.seb4u.demo.spring oder com.seb4u.demo.jakarta. Externe Imports wie org.springframework... oder jakarta.validation... bleiben unverändert.
package com.seb4u.demo.spring.orders.api;

import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
final class OrderController {
    @PostMapping("/orders")
    OrderResponse place(@Valid @RequestBody PlaceOrderRequest request) {
        return new OrderResponse("accepted", request.customerId());
    }

    record PlaceOrderRequest(String customerId) {}
    record OrderResponse(String status, String customerId) {}
}

Modulverzeichnis: alle Inhalte nach Thema gruppiert

Neue Hauptstruktur · nach Modulen statt nach Nummern sortiert
Strukturprinzip: Die Inhalte sind nach Lernbereichen gruppiert. Die früheren Kapitelnummern dienen nur noch als kleine Referenz, aber nicht mehr als Hauptnavigation.

Modul A · Orientierung, Lernarchitektur & Verzeichnis

Start, Bedienung, Roadmap, Lernpfade, Strukturentscheidungen und PDF/Print-Strategie.

  1. Java Master-Handbuch: Modulstruktur Edition
  2. Akzeptierte Gesamtstruktur: vom Java-Core bis zur Senior-/Projekt-Reife
  3. Visuelle Gesamtkarte
  4. So benutzt du dieses Master-Handbuch · K0
  5. 24-Wochen-Masterplan · K29
  6. Master-Checklisten, Interviewfragen und Portfolio · K30
  7. Java Master-Handbuch
  8. Strukturcheck und neue Lernarchitektur · K81
  9. PDF und Print Edition Strategie · K104

Modul B · Core Java, Sprache & Typsystem

JDK, Syntax, OOP, Records, Collections, Generics, Streams und Fehlerverträge inklusive Deep Dives.

  1. Java-Ökosystem, JDK und Toolchain · K1
  2. Vom Quellcode zum laufenden Programm · K2
  3. Syntax, Typen und Werte · K3
  4. Kontrollfluss und Fehlerdenken · K4
  5. Methoden, API-Design und Lesbarkeit · K5
  6. Objektorientierung richtig verstehen · K6
  7. Records, Enums und sealed Types · K7
  8. Collections und Datenstrukturen · K8
  9. Generics und Typsicherheit · K9
  10. Lambdas, Streams und funktionales Denken · K10
  11. Exceptions und robuste Fehlerverträge · K11
  12. Deep Dive: Records, Enums und sealed Types · K46
  13. Deep Dive: Collections und Datenstrukturen · K47
  14. Deep Dive: Generics und Typsicherheit · K48
  15. Deep Dive: Lambdas, Streams und funktionales Denken · K49
  16. Deep Dive: Exceptions und robuste Fehlerverträge · K50

Modul C · Runtime, I/O, Zeit & Concurrency

Dateien, NIO.2, Ressourcen, Zeit/Locale, Virtual Threads, Async-Workflows, Speicher, GC, Module und Builds.

  1. I/O, Dateien, NIO.2 und Ressourcen · K12
  2. Datum, Zeit, Locale und Text · K13
  3. Nebenläufigkeit: Threads bis Virtual Threads · K14
  4. CompletableFuture, Timeouts und asynchrone Workflows · K15
  5. Speicher, Stack, Heap und Garbage Collection · K16
  6. Module, Packages und Architekturgrenzen · K17
  7. Build, Dependencies und reproduzierbare Projekte · K18
  8. Java I/O & NIO.2 – Produktionsreife Datei- und Datenverarbeitung · K44
  9. Java Concurrency – Virtual Threads, Backpressure, Locks und Diagnose · K45
  10. Deep Dive: I/O, NIO.2 und Ressourcen – Enterprise-Dateipipelines · K51
  11. Deep Dive: Datum, Zeit, Locale und Text – fachlich korrekt rechnen · K52
  12. Deep Dive: Nebenläufigkeit – kontrollierte Parallelität statt Thread-Chaos · K53
  13. Deep Dive: CompletableFuture – Orchestrierung, Fallbacks und Hedging · K54
  14. Deep Dive: Speicher, GC und Leak-Diagnose – messen, begrenzen, freigeben · K55
  15. Deep Dive: Module, Packages und Architekturgrenzen – Codebase skalierbar halten · K56
  16. Deep Dive: Build, Dependencies und reproduzierbare Projekte · K57

Modul D · Professional Backend, Architektur & Code-Labs

Testing, HTTP, JDBC, Security, Patterns, DDD, Performance, Observability, Refactoring und komplexe Code-Labs.

  1. Testing: Von Unit bis Contract · K19
  2. HTTP, REST, JSON und Systemgrenzen · K20
  3. Datenbanken, JDBC und Transaktionen · K21
  4. Sicherheit: Eingaben, Secrets und Rechte · K22
  5. Design Patterns ohne Dogma · K23
  6. Domain-Driven Design und Hexagonale Architektur · K24
  7. Performance: Messen, nicht raten · K25
  8. Observability, Logging und Diagnose · K26
  9. Refactoring und Clean Code für Fortgeschrittene · K27
  10. Großes Master-Beispiel: Event-Sourced Ledger API · K28
  11. Komplexe Java-Code-Labs
  12. Code-Lab: Hexagonale Modulstruktur · K31
  13. Code-Lab: Result, Validation und Fehler ohne Exception-Spaghetti · K32
  14. Code-Lab: Event-Sourcing mit Snapshots und Projektionen · K33
  15. Code-Lab: JDBC Repository mit Transaktion und Optimistic Locking · K34
  16. Code-Lab: Virtual Threads, Bulkhead und kontrollierte Parallelität · K35
  17. Code-Lab: CompletableFuture-Orchestrierung mit Fallbacks · K36
  18. Code-Lab: Flow API mit Backpressure · K37
  19. Code-Lab: Type-safe Builder mit Staged Interfaces · K38
  20. Code-Lab: Specification Pattern und Query DSL · K39
  21. Code-Lab: Passwort-Hashing mit Standard-JDK · K40
  22. Code-Lab: Testbare Zeit, IDs und Ports · K41
  23. Code-Lab: Strukturierte Logs und einfache Metriken ohne Framework · K42
  24. Zusätzliche SVG-Lernbilder
  25. Code-Atlas pro Thema: mehrere komplexe Beispielcodes · K43

Modul E · CommerceFlow Master-Projekt

Durchgehendes Projekt mit Domain Core, Application Layer, HTTP/JSON, Outbox, Worker, Tests und Runbook.

  1. Durchgehendes Master-Projekt: CommerceFlow – Architekturüberblick · K58
  2. Master-Projekt: Domain Core – Order, Money, Events und Invarianten · K59
  3. Master-Projekt: Application Layer – Use Cases, Ports und Fehlerverträge · K60
  4. Master-Projekt: HTTP/JSON Adapter – klare Systemgrenze · K61
  5. Master-Projekt: I/O Adapter – Outbox, Snapshots und idempotente Dateien · K62
  6. Master-Projekt: Concurrency Worker – Imports, Outbox und kontrollierte Parallelität · K63
  7. Master-Projekt: Tests, Build und Runbook · K64

Modul F · Enterprise Java: Spring, Persistence, Testing, Security & Production

Spring Boot, Datenbank/Persistence, Testing, Security, Production Readiness, Enterprise Upgrade und Release Runbook.

  1. Enterprise Edition Überblick: Von Java Core zu Production Java · K65
  2. Spring Boot Professional Deep Dive · K66
  3. Datenbank & Persistence Masterclass · K67
  4. Testing Masterclass: Unit, Integration, Contract, Architektur · K68
  5. Security Deep Dive: Auth, JWT, OWASP, Secrets · K69
  6. Production Readiness: Logs, Metrics, Health, Docker, CI/CD · K70
  7. CommerceFlow Enterprise Upgrade · K71
  8. Enterprise Runbook & Release Checklist · K72

Modul G · Job-Ready, Aufgaben, Code Review & Interview

Aufgaben mit Lösungen, Review-Fallen, Senior-Interviewfragen, Architektur-Cases und Portfolio-Pack.

  1. Job-Ready Edition Überblick · K73
  2. Aufgaben & Lösungen: Core Java Mastery · K74
  3. Aufgaben & Lösungen: I/O und Concurrency · K75
  4. Aufgaben & Lösungen: Spring, Data, Security · K76
  5. Code-Review-Fallen: Schlechter Code zu professionellem Code · K77
  6. Senior Java Interviewfragen mit Musterantworten · K78
  7. Architektur-Cases für Senior-Gespräche · K79
  8. Portfolio- und Bewerbungs-Pack · K80

Modul I · Microservices, Messaging & Cloud

Gateway, Service Discovery, Kafka/RabbitMQ, Outbox, Sagas, Resilience, Docker, Kubernetes, Observability und CI/CD.

  1. Microservices und Cloud Edition Überblick · K89
  2. API Gateway, Service Discovery und Konfiguration · K90
  3. Messaging, Kafka/RabbitMQ und Outbox Pattern · K91
  4. Verteilte Transaktionen, Saga und Idempotenz · K92
  5. Resilience: Circuit Breaker, Rate Limiting und Bulkheads · K93
  6. Docker Compose und Kubernetes Grundlagen · K94
  7. Cloud Observability: Logs, Metrics, Traces · K95
  8. Cloud Release Checklist und CI/CD · K96

Modul J · Algorithmen & Datenstruktur Mastery

Big-O, Collections-Auswahl, HashMap, Sliding Window, Graphen, DP, Backtracking, Trie und Interview-Patterns.

  1. Algorithmus und Datenstruktur Mastery Überblick · K97
  2. Big-O, Benchmark-Denken und Collection-Auswahl · K98
  3. HashMap, Sets und Frequency Patterns · K99
  4. Arrays, Strings, Sliding Window und Two Pointers · K100
  5. Graphen: BFS, DFS, Dijkstra, Union-Find · K101
  6. Dynamic Programming, Backtracking und Trie · K102
  7. Algorithmus Interview Patterns und Musterantworten · K103

Modul K · Quellen, Versionshinweise & Anhang

Quellen, Versionshinweise, Qualitätscheck und ergänzende Hinweise.

  1. Quellen und Versionshinweise

Visuelle Gesamtkarte

Modul A · Orientierung, Lernarchitektur & Verzeichnis
Java Master Map Java Master Map Java Mastery Sprache Typen OOP Generics Runtime JVM GC Threads Backend HTTP JDBC Security Architektur DDD Testing Observability Diese Karte verbindet Syntax, Runtime und professionelle Softwareentwicklung.
🧭

So benutzt du dieses Master-Handbuch

Modul A · Orientierung, Lernarchitektur & Verzeichnis · Alt-Referenz: K0

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Lernstrategie: Lesen → Bauen → Reflektieren → Wiederholen Lernstrategie: Lesen → Bauen → Reflektieren → Wiederholen Konzept lesen Code tippen Fehler suchen Mini-Projekt Review Jedes Kapitel endet mit Übungen, Kriterien und Transferaufgaben.

Dieses Handbuch ist als kompletter Java-Masterpfad aufgebaut: Du lernst nicht nur Syntax, sondern Denkmodelle, Architektur, Testing, Nebenläufigkeit, Performance und Produktionsreife. Arbeite nicht passiv durch die Kapitel. Lies ein Thema, schreibe den Code manuell ab, verändere ihn absichtlich, provoziere Fehler und erkläre anschließend, warum die Fehler entstehen.

Der Pfad ist auf JDK 25 LTS ausgerichtet. Das bedeutet: Du lernst eine moderne, produktionsnahe Java-Basis. Features aus neueren Feature-Releases oder Preview-APIs solltest du nur dann einsetzen, wenn du ihre Stabilität und Deploymentsituation bewusst bewertest.

Master-Regel: Ein Thema gilt erst als verstanden, wenn du es in eigenem Code anwenden, testen, debuggen und einer anderen Person erklären kannst.

Arbeitsrhythmus: 60 Prozent bauen, 20 Prozent lesen, 10 Prozent refaktorieren, 10 Prozent dokumentieren. Nutze kleine Commits und schreibe nach jeder Einheit drei Sätze: Was habe ich gelernt? Was war unklar? Welcher Fehler war wertvoll?

Übungen

  • Lege ein Git-Repository `java-master-lab` an.
  • Erstelle pro Kapitel ein Paket, z. B. `chapter03.collections`.
  • Führe ein Lernlog als Markdown-Datei.

Mastery-Check

  • Du kannst JDK, JRE, JVM und Java SE unterscheiden.
  • Du kompilierst und startest Java-Dateien ohne IDE.
  • Du weißt, welche Kapitel für Backend, Android, Cloud oder Forschung besonders wichtig sind.
📅

24-Wochen-Masterplan

Modul A · Orientierung, Lernarchitektur & Verzeichnis · Alt-Referenz: K29

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

24 Wochen in 6 Phasen 24 Wochen in 6 Phasen 1-4 Basis 5-8 OOP 9-12 APIs 13-16 Concurrency 17-20 Architektur 21-24 Masterprojekt Jede Phase endet mit einem lauffähigen Artefakt.

Wochen 1–4: Fundament. Toolchain, Syntax, Typen, Kontrollfluss, Methoden, Debugging. Artefakt: CLI-Rechner mit Tests und Dateiimport.

Wochen 5–8: Objektmodellierung. OOP, Records, sealed Types, Collections, Generics. Artefakt: Bestellsystem mit Domainmodell, Validierung und Unit Tests.

Wochen 9–12: APIs und Persistenz. I/O, Zeit, HTTP, JDBC, Transaktionen. Artefakt: REST-nahe API oder CLI-Service mit SQL-Datenbank.

Wochen 13–16: Nebenläufigkeit und Robustheit. Threads, Virtual Threads, Futures, Timeouts, Fehlergrenzen. Artefakt: paralleler Importer mit Backpressure und Fehlerreport.

Wochen 17–20: Architektur und Qualität. Module, Ports/Adapter, DDD, Testing, Security. Artefakt: hexagonal aufgebauter Service mit Build-Pipeline.

Wochen 21–24: Masterprojekt. Event-Sourced Ledger, Shop, Booking Engine oder Task-Orchestrator. Artefakt: dokumentiertes Projekt mit Tests, Observability-Konzept, Performancebericht und Architekturentscheidung.

Übungen

  • Plane pro Woche drei Lernblöcke.
  • Definiere für jede Phase ein messbares Artefakt.
  • Führe am Ende jeder Phase ein Review durch.

Mastery-Check

  • Du hast einen realistischen Zeitplan.
  • Du baust jede Phase praktisch ab.
  • Du sammelst Portfolio-Artefakte.
🎓

Master-Checklisten, Interviewfragen und Portfolio

Modul A · Orientierung, Lernarchitektur & Verzeichnis · Alt-Referenz: K30

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Mastery Radar Mastery Radar Erklären Implementieren Testen Debuggen Syntax ~ OOP Concurrency ~ ~ ~ Architektur ~ ~ Performance ~ ~ Mastery entsteht, wenn Wissen in mehreren Kontexten abrufbar ist.

Nutze diese Abschlussfragen als Selbstprüfung:

1. Warum ist `List<String>` keine Unterklasse von `List<Object>`? 2. Wann ist ein Record besser als eine Klasse? 3. Wie entsteht eine Race Condition? 4. Warum kann ein Java-Programm ein Memory Leak haben? 5. Wo übersetzt du technische Exceptions in API-Fehler? 6. Welche Datenstruktur wählst du für schnelle Schlüssel-Suche? 7. Warum sind Timeouts in HTTP-Clients Pflicht? 8. Was ist der Unterschied zwischen Entity und Value Object? 9. Warum sind Tests auch Designfeedback? 10. Was würdest du messen, bevor du Performance optimierst?

Portfolio-Idee: Veröffentliche ein Projekt mit sauberer README, Architekturdiagramm, Tests, Build-Anleitung, Domainmodell, API-Beispielen, Fehlerstrategie und einem kurzen Performance-Abschnitt. Ein gutes Portfolio zeigt Entscheidungen, nicht nur Code.

Übungen

  • Beantworte alle zehn Fragen schriftlich.
  • Erstelle eine README für dein Masterprojekt.
  • Nimm ein 5-Minuten-Video auf, in dem du Architektur und Trade-offs erklärst.

Mastery-Check

  • Du kannst Java-Konzepte mündlich erklären.
  • Du besitzt ein vorzeigbares Projekt.
  • Du kennst deine nächsten Lernlücken.
🧩
Finale Erweiterung

Strukturcheck und neue Lernarchitektur

Modul A · Orientierung, Lernarchitektur & Verzeichnis · Alt-Referenz: K81

Gesamtaufbau prüfen, Lernspuren definieren und die nächsten Erweiterungen sauber einordnen.

Struktur-Audit: vom Lernhandbuch zur Master-Akademie

Diese Erweiterung prüft die vorhandene Reihenfolge und ordnet die nächsten Inhalte als klare Lernpfade. Die Basis bleibt erhalten; neu ist eine sichtbare Progression von Core Java über Enterprise bis Cloud und Interview-Mastery.

BasisKapitel 1-18: Sprache, Runtime, I/O, Concurrency, Build
PraxisKapitel 19-72: Architektur, Testing, Security, Produktion
JobKapitel 73-80: Aufgaben, Reviews, Interviews, Portfolio
NeuKapitel 81-104: Fullstack, Cloud, Algorithmen, Print
Neue Gesamtstruktur des Java-Master-Handbuchs Neue Gesamtstruktur des Java-Master-Handbuchs Core Java Syntax, Typmodell, Collections Runtime JVM, I/O, Concurrency Enterprise Spring, Data, Security Fullstack UI, REST, Uploads, Exports Cloud Microservices, Kafka, Kubernetes Mastery Algorithmen, Interviews, Portfolio Die Kapitel sind jetzt als aufeinander aufbauende Lernspuren lesbar.
BereichStatusEmpfehlung
Core JavaSehr stark ausgebautBeibehalten; Kapitel 7-18 dienen als technische Referenz.
EnterpriseStark ausgebautMit Fullstack- und Cloud-Erweiterung als reale Projektlinie verknüpfen.
Job-ReadyVorhandenMit Algorithmen-Teil und Portfolio-Cases verbinden.
PrintFehlt als eigener OutputEigene Druck/PDF-Datei mit Seitenumbrüchen, Cover und Code-freundlichem Layout.

Lernspur A: Java Master

Kapitel 1-18, danach I/O, Concurrency und Records/Generics/Streams Deep Dives.

Lernspur B: Backend Engineer

Kapitel 19-72, danach CommerceFlow Enterprise und REST/Data/Security.

Lernspur C: Fullstack Engineer

Neue Kapitel 82-88 mit UI, Forms, Upload, Export und Frontend-Integration.

Lernspur D: Senior/Cloud

Neue Kapitel 89-96 mit Microservices, Outbox, Saga, Docker, Kubernetes und Observability.

Lernspur E: Interview

Kapitel 73-80 plus neue Algorithmus-Kapitel 97-104.

Lernspur F: Print/PDF

Separate Print/PDF-Datei als saubere Druckausgabe.

Strukturentscheidung: Die neuen Kapitel werden nicht zwischen bestehende Kapitel geschoben, sondern ab Kapitel 81 ergänzt. Dadurch bleiben vorhandene Verweise stabil und die Datei bleibt versionierbar.
🖨️
Finale Erweiterung

PDF und Print Edition Strategie

Modul A · Orientierung, Lernarchitektur & Verzeichnis · Referenz: K104

Die große HTML-Datei in eine lesbare Druck- und PDF-Ausgabe überführen.

Print/PDF Edition

Dieses Kapitel dokumentiert die Print-Strategie. Zusätzlich wird eine separate druckfreundliche HTML-Datei und eine PDF-Datei erzeugt. Die Druckversion blendet Sidebar, Suche und Buttons aus, nutzt A4-Seiten, saubere Kapitelumbrüche und kompaktere Codeblöcke.

Cover

Titel, Version, Lernspuren, Umfang und Zielgruppe auf einer eigenen Startseite.

Kapitelumbrüche

Jedes Hauptkapitel beginnt im PDF auf einer neuen Seite.

Code-Lesbarkeit

Code ist kleiner, wrappt aber ohne horizontales Scrollen.

Offline-Fähigkeit

Alle SVGs, CSS und Codebeispiele bleiben eingebettet.

Print-ElementUmsetzung
SidebarIm Print ausgeblendet, weil sie am Papier keinen Nutzen hat.
Copy-ButtonsIm Print ausgeblendet.
SVGsBleiben eingebettet und werden seitenbruchgeschützt.
Details/AccordionIm Print vollständig geöffnet.
CodeMonospace, kleinere Schrift, Zeilenumbruch.
Modul B

☕ Core Java, Sprache & Typsystem

JDK, Syntax, OOP, Records, Collections, Generics, Streams und Fehlerverträge inklusive Deep Dives.

16Themen in diesem Modul
26Codeblöcke
16SVG-Grafiken

Java-Ökosystem, JDK und Toolchain

Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K1

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Java-Plattform in Schichten Java-Plattform in Schichten Dein Code: .java, Tests, Ressourcen Javac: Parser, Typprüfung, Bytecode JDK-Werkzeuge: jar, jlink, jshell, jdeps, jcmd JVM: Class Loading, JIT, GC, Threads Betriebssystem und Hardware Das JDK ist mehr als ein Compiler: Es ist Werkzeugkasten, Laufzeit und Diagnosebasis.

Java ist Sprache, Plattform und Ökosystem zugleich. Als Sprache liefert Java Syntax, Typmodell, Klassen, Records, Interfaces, Generics, Lambdas und Exceptions. Als Plattform liefert Java Bytecode, die JVM, Garbage Collection, Class Loading, Security-Mechanismen und Diagnosewerkzeuge. Als Ökosystem umfasst Java Build-Tools, Bibliotheken, Frameworks, Application Server, Cloud-Runtimes und Observability-Stacks.

JDK steht für Java Development Kit. Es enthält Compiler, Runtime, Standardbibliothek und Werkzeuge. JRE bezeichnete historisch die reine Laufzeitumgebung; in modernen Distributionen arbeitet man meist direkt mit dem JDK oder mit durch `jlink` erzeugten Runtime-Images. JVM ist die virtuelle Maschine, die Bytecode ausführt.

Wichtige Werkzeuge: `javac` kompiliert, `java` startet Programme, `jar` paketiert, `jshell` erlaubt Experimente, `jdeps` analysiert Modul- und Paketabhängigkeiten, `jcmd` fragt laufende JVMs ab, `jfr` und JDK Mission Control helfen beim Profiling.

Masterblick: Eine Senior-Entwicklerin unterscheidet drei Ebenen: Quellcode-Ebene, Bytecode-Ebene und Runtime-Ebene. Viele Probleme wirken wie „Java ist langsam“, sind aber eigentlich I/O-Wartezeiten, falsche Datenstrukturen, schlechte Queries oder unkontrollierte Objektallokation.

Beispielcode

// HelloToolchain.java
public class HelloToolchain {
    public static void main(String[] args) {
        System.out.println("Java läuft: " + Runtime.version());
    }
}

// Terminal:
// javac --release 25 HelloToolchain.java
// java HelloToolchain
// javap -c HelloToolchain

Übungen

  • Kompiliere eine Datei mit `javac --release 25`.
  • Nutze `javap -c`, um Bytecode sichtbar zu machen.
  • Starte `jshell` und teste Records, Streams und Pattern Matching.

Mastery-Check

  • Du kannst erklären, warum Bytecode plattformunabhängig ist.
  • Du weißt, wann `--release` wichtiger ist als nur `--source`.
  • Du kennst mindestens fünf JDK-Werkzeuge.
⚙️

Vom Quellcode zum laufenden Programm

Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K2

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Kompilieren und Starten Kompilieren und Starten .java Datei javac .class Bytecode Class Loader Interpreter/JIT Maschinencode Die JVM startet häufig interpretierend und optimiert Hot Paths später per JIT.

Der Java-Startprozess ist ein mehrstufiger Ablauf. Zuerst wird Quellcode geparst und typgeprüft. Danach erzeugt `javac` Bytecode. Beim Start lädt der Class Loader die benötigten Klassen. Die JVM verifiziert Bytecode, interpretiert ihn zunächst und kompiliert häufig ausgeführte Pfade später optimierend in nativen Maschinencode.

Der praktische Nutzen dieses Wissens ist groß: Wenn eine Klasse nicht gefunden wird, ist es meist ein Classpath- oder Modulproblem. Wenn Code erst nach einiger Zeit schneller wird, liegt das oft am JIT-Warmup. Wenn ein Produktionsservice beim Start langsam ist, können Class Loading, Reflection, Framework-Scanning oder Initialisierungskosten beteiligt sein.

Fehlerdiagnose: `ClassNotFoundException` bedeutet, dass zur Laufzeit eine Klasse per Name gesucht, aber nicht gefunden wurde. `NoClassDefFoundError` bedeutet oft, dass eine Klasse zur Compile-Zeit vorhanden war, zur Laufzeit aber fehlt oder fehlerhaft initialisiert wurde.

Beispielcode

public class StartPhasen {
    static { System.out.println("1) Klasse wird initialisiert"); }
    public static void main(String[] args) {
        System.out.println("2) main wird ausgeführt");
        helper();
    }
    static void helper() { System.out.println("3) Methode wird aufgerufen"); }
}

Übungen

  • Führe `javap -verbose StartPhasen` aus.
  • Entferne absichtlich eine `.class`-Datei und analysiere die Fehlermeldung.
  • Schreibe eine Klasse mit statischem Initialisierungsblock und beobachte die Reihenfolge.

Mastery-Check

  • Du kannst Compile-Zeit und Laufzeit unterscheiden.
  • Du erkennst Classpath-Probleme schneller.
  • Du verstehst den Begriff JIT-Warmup.
🔤

Syntax, Typen und Werte

Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K3

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Java-Typmodell Java-Typmodell Java-Typen Primitive int long double boolean Referenzen String Array Object Record Spezialfälle null var Generics Primitive speichern Werte direkt; Referenztypen verweisen auf Objekte.

Java ist statisch typisiert: Der Compiler prüft, welche Operationen für welchen Typ erlaubt sind. Primitive Typen wie `int`, `long`, `double` und `boolean` sind keine Objekte. Referenztypen wie `String`, Arrays, Klassen, Records und Interfaces verweisen auf Objekte im Heap.

`var` bedeutet nicht dynamische Typisierung. Der Compiler leitet den statischen Typ nur aus der rechten Seite ab. Nutze `var`, wenn der rechte Ausdruck den Typ klar macht; vermeide es, wenn Lesbarkeit leidet.

Achte auf numerische Fallen: Ganzzahldivision, Überlauf, Rundungsfehler bei `double` und `float`. Für Geldbeträge ist `BigDecimal` meist sinnvoller als `double`, aber auch `BigDecimal` verlangt korrekte Rundungsregeln und Skalierung.

Null ist kein Wertobjekt. Null bedeutet Abwesenheit einer Referenz. Viele robuste Java-APIs vermeiden Null durch klare Vorbedingungen, `Optional` für Rückgaben oder Nullness-Checks an Grenzen.

Beispielcode

import java.math.BigDecimal;

public class TypenDemo {
    public static void main(String[] args) {
        int a = 7 / 2;                  // 3, nicht 3.5
        double b = 0.1 + 0.2;           // nicht exakt 0.3
        BigDecimal price = new BigDecimal("19.99");
        var label = "Preis: " + price;  // var ist statisch String
        System.out.println(a + " | " + b + " | " + label);
    }
}

Übungen

  • Erzeuge Beispiele für int-Overflow.
  • Vergleiche `new BigDecimal(0.1)` mit `new BigDecimal("0.1")`.
  • Schreibe eine Methode, die `null` aktiv ablehnt.

Mastery-Check

  • Du kannst primitive und Referenztypen unterscheiden.
  • Du setzt `var` bewusst ein.
  • Du vermeidest `double` für exakte Geldbeträge.
🚦

Kontrollfluss und Fehlerdenken

Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K4

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Kontrollfluss Kontrollfluss Input if/switch Schleife Methode Rückgabe/Exception Gute Programme machen Entscheidungswege sichtbar und testbar.

Kontrollfluss beantwortet: Welche Codepfade existieren? Wann wird welcher Pfad ausgeführt? Java bietet `if`, `switch`, Schleifen, Methodenaufrufe, Rückgaben und Exceptions. Moderne Switch-Ausdrücke können Werte liefern und machen Zustandsentscheidungen klarer.

Komplexität entsteht selten durch einzelne Anweisungen, sondern durch verschachtelte Bedingungen. Reduziere Verschachtelung durch Guard Clauses, kleine Methoden, klare Namen und testbare Prädikate.

Masterregel: Eine Bedingung, die in natürlicher Sprache schwer erklärbar ist, ist meistens auch im Code schwer wartbar. Extrahiere sie in eine Methode mit Domänenname.

Beispielcode

sealed interface Payment permits Cash, Card, BankTransfer {}
record Cash() implements Payment {}
record Card(String last4) implements Payment {}
record BankTransfer(String iban) implements Payment {}

class PaymentRouter {
    static String route(Payment p) {
        return switch (p) {
            case Cash c -> "Kasse";
            case Card c when c.last4().startsWith("4") -> "Visa-Netzwerk";
            case Card c -> "Kartennetzwerk";
            case BankTransfer b -> "SEPA";
        };
    }
}

Übungen

  • Ersetze verschachtelte `if`-Blöcke durch Guard Clauses.
  • Schreibe einen `switch` über ein sealed interface.
  • Finde drei Bedingungen und extrahiere sie als Methoden.

Mastery-Check

  • Du erkennst zu tiefe Verschachtelung.
  • Du nutzt `switch` nicht nur als altes C-Konstrukt.
  • Du kannst Kontrollfluss mit Tests abdecken.
📐

Methoden, API-Design und Lesbarkeit

Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K5

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Gute Methode Gute Methode Name Parameter Vorbedingung Operation Rückgabe Fehlervertrag Eine Methode ist ein Vertrag, nicht nur ein Codeblock.

Methoden sind die kleinste API-Einheit. Gute Methoden haben klare Namen, wenige Parameter, erkennbare Vorbedingungen und einen sauberen Rückgabevertrag. Eine Methode sollte auf einer Abstraktionsebene bleiben: Nicht gleichzeitig HTTP parsen, Daten validieren, SQL bauen und HTML formatieren.

Parameterlisten mit mehr als drei oder vier Parametern deuten oft auf fehlende Value Objects hin. Ersetze zusammengehörige Daten durch Records, etwa `Money`, `EmailAddress`, `DateRange` oder `TransferRequest`.

Seiteneffekte sollten bewusst sein. Eine Methode, die Datenbankzustand verändert, Logs schreibt oder Netzwerkaufrufe macht, ist schwieriger zu testen als eine pure Berechnung.

Beispielcode

record DateRange(java.time.LocalDate from, java.time.LocalDate to) {
    DateRange {
        if (from.isAfter(to)) throw new IllegalArgumentException("from > to");
    }
    boolean contains(java.time.LocalDate date) {
        return !date.isBefore(from) && !date.isAfter(to);
    }
}

Übungen

  • Refaktoriere eine Methode mit fünf Parametern zu einem Record.
  • Schreibe eine pure Methode und teste sie ohne Mocks.
  • Dokumentiere Vorbedingungen mit Exceptions.

Mastery-Check

  • Du entwirfst Methoden als Verträge.
  • Du nutzt Records als Parameterobjekte.
  • Du trennst Berechnung von Seiteneffekten.
🧱

Objektorientierung richtig verstehen

Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K6

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

OOP-Beziehungen OOP-Beziehungen Objektmodell is-a extends implements Polymorphie has-a Komposition Aggregation Delegation uses-a Service Repository Adapter Komposition ist in Fachlogik oft robuster als tiefe Vererbung.

Objektorientierung ist nicht „alles in Klassen packen“. Gute OOP modelliert Verantwortlichkeiten, Identität, Verhalten und Zusammenarbeit. Eine Klasse sollte einen klaren Grund haben, sich zu ändern. Ein Objekt schützt seine Invarianten und bietet Operationen, die fachlich sinnvoll sind.

Vererbung beschreibt eine echte „ist-ein“-Beziehung. Zu viel Vererbung führt zu starren Hierarchien. Komposition ist häufig flexibler: Ein Objekt enthält andere Objekte und delegiert Verhalten.

Polymorphie bedeutet, dass unterschiedliches Verhalten über gemeinsame Schnittstellen aufrufbar ist. Das reduziert `if`-Ketten und macht Erweiterungen sauberer.

Beispielcode

interface DiscountPolicy { Money apply(Money subtotal); }
record NoDiscount() implements DiscountPolicy { public Money apply(Money subtotal) { return subtotal; } }
record PercentageDiscount(java.math.BigDecimal percent) implements DiscountPolicy {
    public Money apply(Money subtotal) {
        var factor = java.math.BigDecimal.ONE.subtract(percent);
        return new Money(subtotal.amount().multiply(factor), subtotal.currency());
    }
}
record Money(java.math.BigDecimal amount, java.util.Currency currency) {}

Übungen

  • Finde eine Stelle mit `if(type == ...)` und ersetze sie durch Polymorphie.
  • Modelliere `Order`, `OrderLine` und `ProductId` mit Invarianten.
  • Zeichne deine Klassen als SVG oder UML-Skizze.

Mastery-Check

  • Du unterscheidest Identität und Wertobjekt.
  • Du bevorzugst Komposition vor tiefer Vererbung.
  • Du modellierst fachliches Verhalten in Domänenobjekten.
🔒

Records, Enums und sealed Types

Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K7

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Moderne Domänenmodellierung Moderne Domänenmodellierung Record: unveränderliche Datenstruktur Enum: begrenzte Konstantenmenge sealed interface: geschlossene Typfamilie Pattern Matching: sichere Fallunterscheidung Diese Features machen Domänenmodelle kompakter und expliziter.

Records eignen sich für transparente, unveränderliche Datencontainer mit Wertsemantik. Sie erzeugen Konstruktor, Accessors, `equals`, `hashCode` und `toString`. Mit kompakten Konstruktoren kannst du Invarianten prüfen.

Enums eignen sich für begrenzte Zustände oder Strategien. Sealed Types beschränken, welche Klassen ein Interface oder eine Klasse erweitern dürfen. Dadurch kann der Compiler bei Pattern Matching prüfen, ob alle Fälle behandelt wurden.

Mastereinsatz: Kombiniere Records und sealed interfaces für Commands, Events, Domain Results und AST-Modelle. Das ergibt Code, der leichter zu prüfen und schwerer falsch zu erweitern ist.

Beispielcode

sealed interface LoginResult permits LoginSuccess, LoginFailure, PasswordExpired {}
record LoginSuccess(String userId) implements LoginResult {}
record LoginFailure(String reason) implements LoginResult {}
record PasswordExpired(String resetToken) implements LoginResult {}

String message(LoginResult result) {
    return switch (result) {
        case LoginSuccess s -> "Willkommen " + s.userId();
        case LoginFailure f -> "Fehler: " + f.reason();
        case PasswordExpired p -> "Passwort erneuern: " + p.resetToken();
    };
}

Übungen

  • Modelliere Result-Typen ohne `null`.
  • Erstelle ein sealed interface für Zahlungsvorgänge.
  • Baue einen Switch, der beim Hinzufügen eines neuen Typs Compilerfeedback gibt.

Mastery-Check

  • Du weißt, wann ein Record sinnvoll ist.
  • Du nutzt sealed Types für geschlossene Fachfamilien.
  • Du vermeidest magische Strings für Zustände.
🗂️

Collections und Datenstrukturen

Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K8

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Collection-Auswahl Collection-Auswahl Reihenfolge Duplikate Key Lookup FIFO List - ~ Set ~ - - ~ Map ~ Keys eindeutig - Queue ~ - Die richtige Datenstruktur ist oft wichtiger als Mikrooptimierung.

Collections sind Alltag in Java. `List` ist für geordnete Sequenzen, `Set` für eindeutige Elemente, `Map` für Schlüssel-Wert-Zuordnung und `Queue` für Warteschlangen. Die Schnittstelle sagt, was du brauchst; die Implementierung bestimmt Performance-Eigenschaften.

`ArrayList` ist schnell für Indexzugriff und Anhängen. `LinkedList` ist selten die beste Wahl. `HashSet` und `HashMap` brauchen korrekte `equals`/`hashCode`. `TreeMap` und `TreeSet` halten sortierte Ordnung. `ConcurrentHashMap` ist für nebenläufigen Zugriff geeignet, ersetzt aber kein vollständiges Transaktionsmodell.

Masterblick: Wähle Collections nach Zugriffsmuster: suchen, sortieren, iterieren, einfügen, löschen, gruppieren. Miss bei Unsicherheit mit realistischen Daten.

Beispielcode

record CustomerId(String value) {}
record Customer(CustomerId id, String name, String city) {}

Map<String, List<Customer>> byCity(List<Customer> customers) {
    return customers.stream()
        .collect(java.util.stream.Collectors.groupingBy(Customer::city));
}

Übungen

  • Implementiere `equals`/`hashCode` falsch und beobachte HashSet-Verhalten.
  • Vergleiche `List`, `Set` und `Map` in einer Suchaufgabe.
  • Schreibe einen kleinen LRU-Cache mit `LinkedHashMap`.

Mastery-Check

  • Du wählst Collections anhand des Zugriffsmusters.
  • Du verstehst Hashing-Grundlagen.
  • Du nutzt unveränderliche Kopien, wenn Daten geschützt werden sollen.
🧬

Generics und Typsicherheit

Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K9

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Generics Generics Compile-Time Typprüfung Type Erasure Raw Types vermeiden Bounds Wildcards Generics schützen dich vor Laufzeitfehlern, verschwinden aber größtenteils zur Laufzeit.

Generics erlauben typsichere Wiederverwendung. `List<String>` ist nicht dasselbe wie `List<Object>`, obwohl `String` ein `Object` ist. Das verhindert unsichere Schreiboperationen. Wildcards folgen der PECS-Regel: Producer Extends, Consumer Super.

Type Erasure bedeutet, dass viele generische Typinformationen zur Laufzeit nicht direkt vorhanden sind. Deshalb kannst du nicht einfach `new T()` schreiben oder zuverlässig `instanceof List<String>` prüfen.

Masterregel: Öffentliche APIs sollten generisch genug sein, aber nicht kryptisch. Ein schwer lesbarer Generic-Typ kann mehr Schaden als Nutzen stiften.

Beispielcode

static double sum(java.util.List<? extends Number> numbers) {
    return numbers.stream().mapToDouble(Number::doubleValue).sum();
}

static void addDefaults(java.util.List<? super Integer> target) {
    target.add(1);
    target.add(2);
}

Übungen

  • Erkläre, warum `List<String>` keine `List<Object>` ist.
  • Schreibe eine generische `Repository<T, ID>`-Schnittstelle.
  • Nutze `? extends` und `? super` in je einem Beispiel.

Mastery-Check

  • Du kannst PECS erklären.
  • Du vermeidest Raw Types.
  • Du kennst Grenzen von Type Erasure.
🌊

Lambdas, Streams und funktionales Denken

Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K10

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Stream Pipeline Stream Pipeline Quelle filter map sorted collect Streams beschreiben Datenflüsse deklarativ, nicht Schritt-für-Schritt-Mutation.

Streams sind kein Ersatz für jede Schleife, aber stark für Transformationen, Filterung, Gruppierung und Aggregation. Eine Stream-Pipeline sollte lesbar bleiben. Wenn du komplexe Seiteneffekte in `map` oder `peek` versteckst, verlierst du die Vorteile.

Lambdas sind an Functional Interfaces gebunden. `Predicate<T>`, `Function<T,R>`, `Consumer<T>` und `Supplier<T>` sind Standardbausteine. Method References sind nützlich, wenn sie lesbarer sind als Lambdas.

Parallel Streams sind kein automatischer Turbo. Sie können bei CPU-lastigen, unabhängigen Operationen helfen, aber bei I/O, Seiteneffekten oder kleinen Datenmengen schaden.

Beispielcode

record Sale(String region, String product, java.math.BigDecimal amount) {}

Map<String, java.math.BigDecimal> revenueByRegion(List<Sale> sales) {
    return sales.stream()
        .filter(s -> s.amount().signum() > 0)
        .collect(java.util.stream.Collectors.groupingBy(
            Sale::region,
            java.util.stream.Collectors.mapping(
                Sale::amount,
                java.util.stream.Collectors.reducing(
                    java.math.BigDecimal.ZERO, java.math.BigDecimal::add
                )
            )
        ));
}

Übungen

  • Ersetze eine Schleife durch eine Stream-Pipeline.
  • Refaktoriere eine zu lange Pipeline in benannte Methoden.
  • Vergleiche sequentiellen und parallelen Stream mit Messung.

Mastery-Check

  • Du erkennst gute und schlechte Stream-Einsätze.
  • Du nutzt Collector-Kombinationen bewusst.
  • Du vermeidest Seiteneffekte in Pipelines.
🧯

Exceptions und robuste Fehlerverträge

Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K11

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Fehlerfluss Fehlerfluss Ursache Exception Boundary Mapping Log Antwort Exceptions sind Teil der API und brauchen klare Grenzen.

Exceptions sind nicht nur Fehlermeldungen, sondern Fehlerverträge. Unchecked Exceptions eignen sich für Programmierfehler und verletzte Vorbedingungen. Checked Exceptions erzwingen Behandlung, können aber APIs schwerfällig machen. Wichtig ist die Grenze: Wo wird ein technischer Fehler in eine fachliche Antwort übersetzt?

Fange Exceptions nicht zu früh. Ein `catch(Exception e)` ohne Kontext verschleiert Ursachen. Logge nicht überall dieselbe Exception mehrfach. Übersetze Fehler an Systemgrenzen: HTTP, CLI, Message Queue, UI.

Masterregel: Eine gute Exception sagt, was passiert ist, wo es passiert ist und ob der Fehler wiederholbar, fachlich oder technisch ist.

Beispielcode

class PaymentException extends RuntimeException {
    PaymentException(String message, Throwable cause) { super(message, cause); }
}

PaymentResult pay(PaymentRequest request) {
    try {
        return gateway.charge(request);
    } catch (java.net.http.HttpTimeoutException e) {
        throw new PaymentException("payment_gateway_timeout", e);
    }
}

Übungen

  • Entwirf eine kleine Exception-Hierarchie für Bestellungen.
  • Baue ein Error-Mapping für HTTP-Statuscodes.
  • Entferne ein sinnloses `catch(Exception)` und verbessere Kontext.

Mastery-Check

  • Du behandelst Exceptions an sinnvollen Grenzen.
  • Du loggst nicht doppelt.
  • Du unterscheidest technische und fachliche Fehler.
Vertiefung zu Kapitel 7

Deep Dive: Records, Enums und sealed Types

Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K46

Moderne Java-Domänenmodelle werden stark, wenn du Werte, Zustände und erlaubte Varianten im Typsystem ausdrückst.

Master-Idee

Records sind keine „kurzen Klassen“, sondern Werttypen mit klaren Invarianten. Enums sind nicht nur Konstanten, sondern kleine Strategieträger. Sealed Types beschreiben geschlossene Familien von Möglichkeiten. Zusammen entsteht ein Modell, in dem ungültige Zustände schwerer zu erzeugen sind.

Kapitel 7 Deep Dive: robuste Domänenformen Kapitel 7 Deep Dive: robuste Domänenformen Record Wert + Invariante Enum begrenzte Auswahl sealed Type geschlossene Familie switch exhaustive Logik Event versionierbar Die Architektur wird stabiler, wenn Varianten explizit und vollständig modelliert sind.
Record richtig verwendenKompakte Datenform, Validierung im kanonischen Konstruktor, keine versteckten Seiteneffekte.
Enum richtig verwendenFür feste, fachliche Kategorien; nicht für dynamische Daten aus der Datenbank.
sealed richtig verwendenFür geschlossene Protokolle, Commands, Events, Fehler und Zustände.
Pattern MatchingFührt fachliche Vollständigkeit in Reviews ein: jeder neue Fall erzwingt Anpassungen.
KonstruktNimm es fürMeide es für
recordValue Objects, DTOs, Events, kleine unveränderliche AntwortenObjekte mit komplexem Lebenszyklus, Identität und mutierendem Zustand
enumfeste Wertebereiche, Status, Strategien mit wenigen VariantenMandanten-/DB-konfigurierbare Regeln
sealed interfacegeschlossene Ereignis-, Command-, Result- und FehlerfamilienPlugin-Systeme mit unbekannten Implementierungen
switch über Typenexhaustive Fallunterscheidung an ArchitekturgrenzenBusinesslogik, die durch Polymorphie einfacher wäre
  1. Modelliere zuerst den fachlichen Wert, dann erst die Persistenzform.
  2. Prüfe jede Record-Komponente auf Null, Format und Wertebereich.
  3. Nutze sealed Types für Varianten, die im Release-Zyklus deines Codes kontrolliert werden.
  4. Halte Mapping-Code an der Grenze; Domänenrecords sollten nicht vom JSON-Framework abhängen.
7.1 Payment-Domäne mit Money-Record, PaymentRail-Enum und sealed Commands

Zeigt Invarianten, exhaustive switch-Verarbeitung und fachliche Varianten ohne Stringly-Typed-Code.

package com.seb4u.demo.domain;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.util.Currency;
import java.util.Objects;
import java.util.UUID;

public final class PaymentModel {
    public record PaymentId(UUID value) {
        public PaymentId {
            Objects.requireNonNull(value, "payment id");
        }
        public static PaymentId newId() { return new PaymentId(UUID.randomUUID()); }
    }

    public record Money(BigDecimal amount, Currency currency) implements Comparable<Money> {
        public Money {
            Objects.requireNonNull(amount, "amount");
            Objects.requireNonNull(currency, "currency");
            amount = amount.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_EVEN);
            if (amount.signum() < 0) throw new IllegalArgumentException("amount must be >= 0");
        }
        public Money plus(Money other) {
            requireSameCurrency(other);
            return new Money(amount.add(other.amount), currency);
        }
        public Money minus(Money other) {
            requireSameCurrency(other);
            if (amount.compareTo(other.amount) < 0) throw new IllegalStateException("insufficient amount");
            return new Money(amount.subtract(other.amount), currency);
        }
        public boolean isZero() { return amount.signum() == 0; }
        private void requireSameCurrency(Money other) {
            if (!currency.equals(other.currency)) {
                throw new IllegalArgumentException("currency mismatch: " + currency + " vs " + other.currency);
            }
        }
        @Override public int compareTo(Money other) {
            requireSameCurrency(other);
            return amount.compareTo(other.amount);
        }
    }

    public enum PaymentRail {
        SEPA(100_000), CARD(10_000), INTERNAL(Integer.MAX_VALUE);
        private final int maxAmount;
        PaymentRail(int maxAmount) { this.maxAmount = maxAmount; }
        boolean supports(Money money) { return money.amount().intValueExact() <= maxAmount; }
    }

    public sealed interface PaymentCommand permits AuthorizePayment, CapturePayment, RefundPayment, CancelPayment {
        PaymentId id();
        Instant requestedAt();
    }

    public record AuthorizePayment(PaymentId id, Money amount, PaymentRail rail, Instant requestedAt)
            implements PaymentCommand {
        public AuthorizePayment {
            Objects.requireNonNull(amount); Objects.requireNonNull(rail); Objects.requireNonNull(requestedAt);
            if (amount.isZero()) throw new IllegalArgumentException("payment amount must be positive");
            if (!rail.supports(amount)) throw new IllegalArgumentException("amount not supported by rail " + rail);
        }
    }
    public record CapturePayment(PaymentId id, Money amount, Instant requestedAt) implements PaymentCommand {}
    public record RefundPayment(PaymentId id, Money amount, String reason, Instant requestedAt) implements PaymentCommand {}
    public record CancelPayment(PaymentId id, String reason, Instant requestedAt) implements PaymentCommand {}

    public static String auditLine(PaymentCommand command) {
        return switch (command) {
            case AuthorizePayment c -> "AUTH " + c.id().value() + " " + c.amount().amount() + " " + c.rail();
            case CapturePayment c   -> "CAPTURE " + c.id().value() + " " + c.amount().amount();
            case RefundPayment c    -> "REFUND " + c.id().value() + " reason=" + c.reason();
            case CancelPayment c    -> "CANCEL " + c.id().value() + " reason=" + c.reason();
        };
    }
}
7.2 Order-State-Machine mit Enum-Status und sealed Events

Ein Zustandsautomat verhindert ungültige Übergänge und macht Terminalzustände explizit.

package com.seb4u.demo.domain;

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

public final class OrderStateMachine {
    public enum OrderStatus {
        DRAFT, SUBMITTED, PAID, FULFILLED, CANCELLED;
        boolean terminal() { return this == FULFILLED || this == CANCELLED; }
    }

    public sealed interface OrderEvent permits OrderSubmitted, PaymentReceived, OrderFulfilled, OrderCancelled {}
    public record OrderSubmitted(Instant at) implements OrderEvent {}
    public record PaymentReceived(String transactionId, Instant at) implements OrderEvent {
        public PaymentReceived { Objects.requireNonNull(transactionId); }
    }
    public record OrderFulfilled(String shipmentId, Instant at) implements OrderEvent {}
    public record OrderCancelled(String reason, Instant at) implements OrderEvent {}

    public record OrderSnapshot(String orderId, OrderStatus status, List<OrderEvent> history) {
        public OrderSnapshot {
            Objects.requireNonNull(orderId); Objects.requireNonNull(status);
            history = List.copyOf(history);
        }
        public OrderSnapshot apply(OrderEvent event) {
            if (status.terminal()) throw new IllegalStateException("terminal order cannot change: " + status);
            OrderStatus next = switch (event) {
                case OrderSubmitted ignored when status == OrderStatus.DRAFT -> OrderStatus.SUBMITTED;
                case PaymentReceived ignored when status == OrderStatus.SUBMITTED -> OrderStatus.PAID;
                case OrderFulfilled ignored when status == OrderStatus.PAID -> OrderStatus.FULFILLED;
                case OrderCancelled ignored -> OrderStatus.CANCELLED;
                default -> throw new IllegalStateException("illegal transition: " + status + " + " + event);
            };
            var nextHistory = new ArrayList<>(history);
            nextHistory.add(event);
            return new OrderSnapshot(orderId, next, nextHistory);
        }
    }
}
7.3 Versionierte Events als stabile Integrationsverträge

Nützlich für Event Sourcing, Messaging und Audit-Logs, wenn Events über mehrere Versionen hinweg lesbar bleiben müssen.

package com.seb4u.demo.domain;

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

public final class VersionedEvents {
    public enum EventVersion {
        V1, V2;
        static EventVersion from(String raw) {
            return raw == null ? V1 : EventVersion.valueOf(raw.toUpperCase());
        }
    }

    public sealed interface AccountEvent permits AccountOpened, EmailChanged, LimitChanged {
        UUID accountId();
        Instant occurredAt();
        EventVersion version();
    }

    public record AccountOpened(UUID accountId, String ownerName, String email, Instant occurredAt, EventVersion version)
            implements AccountEvent {}
    public record EmailChanged(UUID accountId, String email, Instant occurredAt, EventVersion version)
            implements AccountEvent {}
    public record LimitChanged(UUID accountId, int dailyLimit, Instant occurredAt, EventVersion version)
            implements AccountEvent {}

    public static AccountEvent fromEnvelope(Map<String, String> envelope) {
        String type = Objects.requireNonNull(envelope.get("type"), "event type");
        UUID id = UUID.fromString(envelope.get("accountId"));
        Instant at = Instant.parse(envelope.getOrDefault("occurredAt", Instant.EPOCH.toString()));
        EventVersion version = EventVersion.from(envelope.get("version"));
        return switch (type) {
            case "AccountOpened" -> new AccountOpened(id, envelope.get("ownerName"), envelope.get("email"), at, version);
            case "EmailChanged" -> new EmailChanged(id, envelope.get("email"), at, version);
            case "LimitChanged" -> new LimitChanged(id, Integer.parseInt(envelope.get("dailyLimit")), at, version);
            default -> throw new IllegalArgumentException("unknown event type: " + type);
        };
    }

    public static Map<String, String> toEnvelope(AccountEvent event) {
        return switch (event) {
            case AccountOpened e -> Map.of(
                    "type", "AccountOpened", "version", e.version().name(),
                    "accountId", e.accountId().toString(), "ownerName", e.ownerName(),
                    "email", e.email(), "occurredAt", e.occurredAt().toString());
            case EmailChanged e -> Map.of(
                    "type", "EmailChanged", "version", e.version().name(),
                    "accountId", e.accountId().toString(), "email", e.email(),
                    "occurredAt", e.occurredAt().toString());
            case LimitChanged e -> Map.of(
                    "type", "LimitChanged", "version", e.version().name(),
                    "accountId", e.accountId().toString(), "dailyLimit", Integer.toString(e.dailyLimit()),
                    "occurredAt", e.occurredAt().toString());
        };
    }
}
Anti-Pattern:

Records als anämische JSON-Säcke ohne Validierung. Dadurch wandern Fehler später in Services, Mapper oder Datenbankconstraints.

Besser:

Records validieren ihre lokalen Invarianten sofort. Services orchestrieren dann nur noch gültige Werte.

Vertiefung zu Kapitel 8

Deep Dive: Collections und Datenstrukturen

Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K47

Master-Level heißt: Du wählst Datenstrukturen nach Zugriffsmuster, Ordnungsbedarf, Mutierbarkeit, Speicherprofil und Nebenläufigkeit.

Kapitel 8 Deep Dive: von Zugriffsmuster zu Collection Kapitel 8 Deep Dive: von Zugriffsmuster zu Collection Lookup HashMap Reihenfolge LinkedHashMap Sortierung TreeMap Eindeutigkeit Set Pipeline Queue Die beste Collection ist die, deren Kostenmodell zum Use Case passt.
FrageTypische WahlWarum
Schneller Lookup nach Schlüssel?HashMapDurchschnittlich konstante Zugriffszeit; Reihenfolge nicht garantiert.
Lookup plus stabile Iterationsreihenfolge?LinkedHashMapIdeal für LRU-Caches, Importreihenfolge und deterministische Tests.
Bereichsabfragen nach sortiertem Schlüssel?TreeMap / NavigableMapSubmaps, floor/ceiling und sortierte Traversierung.
Sehr wenige Enum-Schlüssel?EnumMapKompakt, schnell und typsicher für Enum-Domänen.
Producer/Consumer?BlockingQueueBackpressure und saubere Übergabe zwischen Threads.
Master-Regel: Collections sind ein Architekturdetail. Sobald du mehrere Zugriffspfade brauchst, baue explizite Indizes und halte sie in einer Transaktion/Operation konsistent.
8.1 Expiring LRU Cache mit LinkedHashMap und TTL

Demonstriert Zugriffreihenfolge, Kapazitätsbegrenzung und testbare Zeit über Clock.

package com.seb4u.demo.collections;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;

public final class ExpiringLruCache<K, V> {
    private final int maxSize;
    private final Duration ttl;
    private final Clock clock;
    private final LinkedHashMap<K, Entry<V>> entries;

    private record Entry<V>(V value, Instant expiresAt) {
        boolean expired(Instant now) { return !expiresAt.isAfter(now); }
    }

    public ExpiringLruCache(int maxSize, Duration ttl, Clock clock) {
        if (maxSize < 1) throw new IllegalArgumentException("maxSize must be positive");
        this.maxSize = maxSize;
        this.ttl = ttl;
        this.clock = clock;
        this.entries = new LinkedHashMap<>(16, 0.75f, true) {
            @Override protected boolean removeEldestEntry(Map.Entry<K, Entry<V>> eldest) {
                return size() > ExpiringLruCache.this.maxSize;
            }
        };
    }

    public synchronized Optional<V> get(K key) {
        Entry<V> entry = entries.get(key);
        if (entry == null) return Optional.empty();
        if (entry.expired(clock.instant())) {
            entries.remove(key);
            return Optional.empty();
        }
        return Optional.of(entry.value());
    }

    public synchronized void put(K key, V value) {
        entries.put(key, new Entry<>(value, clock.instant().plus(ttl)));
    }

    public synchronized int cleanup() {
        Instant now = clock.instant();
        int before = entries.size();
        entries.entrySet().removeIf(e -> e.getValue().expired(now));
        return before - entries.size();
    }
}
8.2 Multi-Index OrderBook mit HashMap, EnumMap und NavigableMap

Ein realistisches In-Memory-Modell mit mehreren sekundären Indizes und konsistenter Indexpflege.

package com.seb4u.demo.collections;

import java.time.Instant;
import java.util.*;

public final class MultiIndexOrderBook {
    public record OrderId(String value) {}
    public enum Status { NEW, PAID, SHIPPED, CANCELLED }
    public record Order(OrderId id, String customerId, Status status, Instant createdAt, long cents) {}

    private final Map<OrderId, Order> byId = new HashMap<>();
    private final Map<String, Set<OrderId>> byCustomer = new HashMap<>();
    private final EnumMap<Status, Set<OrderId>> byStatus = new EnumMap<>(Status.class);
    private final NavigableMap<Instant, Set<OrderId>> byCreatedAt = new TreeMap<>();

    public MultiIndexOrderBook() {
        for (Status s : Status.values()) byStatus.put(s, new LinkedHashSet<>());
    }

    public void upsert(Order order) {
        Objects.requireNonNull(order);
        Order old = byId.put(order.id(), order);
        if (old != null) removeFromIndexes(old);
        addToIndexes(order);
    }

    public Optional<Order> find(OrderId id) { return Optional.ofNullable(byId.get(id)); }

    public List<Order> byCustomer(String customerId) {
        return byCustomer.getOrDefault(customerId, Set.of()).stream().map(byId::get).toList();
    }

    public List<Order> byStatus(Status status) {
        return byStatus.getOrDefault(status, Set.of()).stream().map(byId::get).toList();
    }

    public List<Order> createdBetween(Instant from, Instant to) {
        return byCreatedAt.subMap(from, true, to, true).values().stream()
                .flatMap(Set::stream)
                .map(byId::get)
                .sorted(Comparator.comparing(Order::createdAt))
                .toList();
    }

    private void addToIndexes(Order o) {
        byCustomer.computeIfAbsent(o.customerId(), ignored -> new LinkedHashSet<>()).add(o.id());
        byStatus.get(o.status()).add(o.id());
        byCreatedAt.computeIfAbsent(o.createdAt(), ignored -> new LinkedHashSet<>()).add(o.id());
    }

    private void removeFromIndexes(Order o) {
        remove(byCustomer, o.customerId(), o.id());
        byStatus.get(o.status()).remove(o.id());
        remove(byCreatedAt, o.createdAt(), o.id());
    }

    private static <K, V> void remove(Map<K, Set<V>> index, K key, V value) {
        Set<V> bucket = index.get(key);
        if (bucket == null) return;
        bucket.remove(value);
        if (bucket.isEmpty()) index.remove(key);
    }
}
8.3 Topological Sort mit Zyklenerkennung für Build-/Workflow-Abhängigkeiten

Zeigt, wie Maps, Sets und Queues zusammen ein algorithmisches Problem sauber lösen.

package com.seb4u.demo.collections;

import java.util.*;

public final class DependencyPlanner {
    public record Task(String name, Set<String> dependsOn) {
        public Task { dependsOn = Set.copyOf(dependsOn); }
    }

    public static List<String> topologicalOrder(Collection<Task> tasks) {
        Map<String, Set<String>> remainingDeps = new LinkedHashMap<>();
        Map<String, Set<String>> dependents = new HashMap<>();

        for (Task task : tasks) {
            remainingDeps.put(task.name(), new LinkedHashSet<>(task.dependsOn()));
            for (String dep : task.dependsOn()) {
                dependents.computeIfAbsent(dep, ignored -> new LinkedHashSet<>()).add(task.name());
            }
        }

        ArrayDeque<String> ready = new ArrayDeque<>();
        remainingDeps.forEach((task, deps) -> { if (deps.isEmpty()) ready.add(task); });

        List<String> ordered = new ArrayList<>();
        while (!ready.isEmpty()) {
            String done = ready.removeFirst();
            ordered.add(done);
            for (String dependent : dependents.getOrDefault(done, Set.of())) {
                Set<String> deps = remainingDeps.get(dependent);
                deps.remove(done);
                if (deps.isEmpty()) ready.addLast(dependent);
            }
        }

        if (ordered.size() != remainingDeps.size()) {
            Map<String, Set<String>> cycle = new LinkedHashMap<>();
            remainingDeps.forEach((task, deps) -> { if (!deps.isEmpty()) cycle.put(task, Set.copyOf(deps)); });
            throw new IllegalStateException("cycle detected: " + cycle);
        }
        return ordered;
    }
}
Anti-Pattern:

Alles ist eine ArrayList, danach wird überall linear gesucht, sortiert und gefiltert.

Besser:

Identifiziere die dominanten Operationen: Lookup, Range Query, Ordnung, Einfügen, Entfernen, Speicherverbrauch.

Vertiefung zu Kapitel 9

Deep Dive: Generics und Typsicherheit

Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K48

Generics sind nicht Dekoration. Sie sind ein Werkzeug, um Architekturregeln vom Compiler prüfen zu lassen.

Kapitel 9 Deep Dive: generische API-Grenzen Kapitel 9 Deep Dive: generische API-Grenzen Type Parameter T, ID, E Bounds extends Variance PECS Type Token Class<T> Compiler Regelwächter Gute generische APIs machen falsche Kombinationen unmöglich oder zumindest sichtbar.
KonzeptBedeutungMaster-Hinweis
InvarianzList<Dog> ist keine List<Animal>Schützt davor, falsche Elemente in Collections zu schreiben.
PECSProducer extends, Consumer superFür APIs, die lesen und schreiben, die Varianz bewusst trennen.
Bounds<E extends Entity<ID>>Drückt Beziehungen zwischen Typen aus, nicht nur Einzeltypen.
Type ErasureGenerische Typen sind zur Laufzeit weitgehend gelöschtBei Laufzeit-Typprüfung Type Tokens wie Class<T> verwenden.
  1. Benutze Generics, wenn ein Zusammenhang zwischen Parametern ausgedrückt werden muss.
  2. Gib Typvariablen sprechende Namen, sobald die API öffentlich oder komplex wird.
  3. Nutze Wildcards an Konsum-/Produktionsgrenzen, nicht unnötig im Domänenkern.
  4. Vermeide rohe Typen; sie deaktivieren genau die Sicherheit, die du wolltest.
9.1 Typed IDs und Repository-API mit bounded Generics

Verhindert, dass UserId, InvoiceId und andere technische Strings versehentlich vertauscht werden.

package com.seb4u.demo.generics;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public final class TypedRepositories {
    public sealed interface EntityId permits UserId, InvoiceId { String value(); }
    public record UserId(String value) implements EntityId {}
    public record InvoiceId(String value) implements EntityId {}

    public interface Entity<ID extends EntityId> { ID id(); }
    public record User(UserId id, String email) implements Entity<UserId> {}
    public record Invoice(InvoiceId id, long cents) implements Entity<InvoiceId> {}

    public interface Repository<ID extends EntityId, E extends Entity<ID>> {
        Optional<E> find(ID id);
        void save(E entity);
        List<E> findAll();
    }

    public static final class InMemoryRepository<ID extends EntityId, E extends Entity<ID>>
            implements Repository<ID, E> {
        private final Map<ID, E> store = new ConcurrentHashMap<>();
        @Override public Optional<E> find(ID id) { return Optional.ofNullable(store.get(id)); }
        @Override public void save(E entity) { store.put(entity.id(), entity); }
        @Override public List<E> findAll() { return List.copyOf(store.values()); }
    }

    public static void demo() {
        Repository<UserId, User> users = new InMemoryRepository<>();
        users.save(new User(new UserId("u-1"), "dev@example.com"));

        Repository<InvoiceId, Invoice> invoices = new InMemoryRepository<>();
        invoices.save(new Invoice(new InvoiceId("i-1"), 12_99));

        // users.find(new InvoiceId("i-1")); // kompiliert nicht: genau das ist der Nutzen.
    }
}
9.2 Type-safe heterogene Registry mit Class<T>-Token

Ein fortgeschrittenes Pattern für Konfiguration, Plugin-Kontexte und typisierte Metadaten.

package com.seb4u.demo.generics;

import java.util.*;
import java.util.function.Function;

public final class TypeSafeRegistry {
    public static final class Key<T> {
        private final String name;
        private final Class<T> type;
        private Key(String name, Class<T> type) { this.name = name; this.type = type; }
        public static <T> Key<T> of(String name, Class<T> type) { return new Key<>(name, type); }
        T cast(Object value) { return type.cast(value); }
        @Override public boolean equals(Object o) { return o instanceof Key<?> k && name.equals(k.name) && type.equals(k.type); }
        @Override public int hashCode() { return Objects.hash(name, type); }
    }

    private final Map<Key<?>, Object> values = new HashMap<>();

    public <T> void put(Key<T> key, T value) {
        values.put(key, key.cast(value));
    }

    public <T> Optional<T> get(Key<T> key) {
        return Optional.ofNullable(values.get(key)).map(key::cast);
    }

    public <T, R> Optional<R> map(Key<T> key, Function<? super T, ? extends R> mapper) {
        return get(key).map(mapper);
    }

    public static void demo() {
        Key<Integer> port = Key.of("server.port", Integer.class);
        Key<String> host = Key.of("server.host", String.class);
        var registry = new TypeSafeRegistry();
        registry.put(port, 8080);
        registry.put(host, "localhost");
        String endpoint = registry.map(host, h -> "http://" + h + ":" + registry.get(port).orElseThrow()).orElseThrow();
        System.out.println(endpoint);
    }
}
9.3 Generische Criteria-DSL mit Comparable-Bounds

Zeigt generische Felder, typsichere Prädikate und flexible Filterlogik.

package com.seb4u.demo.generics;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

public final class CriteriaDsl {
    public record Customer(String id, String segment, int age, long lifetimeValue) {}

    public interface Criterion<T> extends Predicate<T> {
        default Criterion<T> and(Criterion<? super T> other) {
            return value -> this.test(value) && other.test(value);
        }
        default Criterion<T> or(Criterion<? super T> other) {
            return value -> this.test(value) || other.test(value);
        }
    }

    public static final class Field<T, V extends Comparable<? super V>> {
        private final String name;
        private final java.util.function.Function<T, V> extractor;
        public Field(String name, java.util.function.Function<T, V> extractor) {
            this.name = name; this.extractor = extractor;
        }
        public Criterion<T> greaterThan(V value) { return row -> extractor.apply(row).compareTo(value) > 0; }
        public Criterion<T> equalTo(V value) { return row -> extractor.apply(row).compareTo(value) == 0; }
        @Override public String toString() { return name; }
    }

    public static <T> List<T> filter(Iterable<T> source, Criterion<? super T> criterion) {
        List<T> result = new ArrayList<>();
        for (T item : source) if (criterion.test(item)) result.add(item);
        return result;
    }

    public static void demo(List<Customer> customers) {
        Field<Customer, Integer> age = new Field<>("age", Customer::age);
        Field<Customer, Long> ltv = new Field<>("lifetimeValue", Customer::lifetimeValue);
        Field<Customer, String> segment = new Field<>("segment", Customer::segment);
        var vipAdults = age.greaterThan(17).and(ltv.greaterThan(100_000L).or(segment.equalTo("VIP")));
        System.out.println(filter(customers, vipAdults));
    }
}
Anti-Pattern:

Map<String,Object> überall, danach Casts und Laufzeitfehler.

Besser:

Nutze Schlüsseltypen, generische Wrapper und Type Tokens, damit Fehler zur Compile-Zeit auffallen.

Vertiefung zu Kapitel 10

Deep Dive: Lambdas, Streams und funktionales Denken

Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K49

Streams sind stark für Transformationen. Sie sind schwach, wenn du Seiteneffekte, Ressourcen und Fehlerverträge ignorierst.

Kapitel 10 Deep Dive: Stream-Pipeline Kapitel 10 Deep Dive: Stream-Pipeline Source Daten Intermediate map/filter Collector Aggregation Boundary Fehler Result Report Professionelle Stream-Pipelines sind lesbar, testbar und haben klare Grenzen.
SituationGute WahlWarnung
Viele reine Transformationenstream().map().filter().toList()Gut lesbar, solange jede Stufe klein und benannt bleibt.
Aggregation mit Zustandeigener CollectorZustand kapseln, Combiner korrekt implementieren.
Sehr große DatenquelleBatching, Iterator, Files.lines mit try-with-resourcesNicht alles mit toList() materialisieren.
I/O pro Elementoft Schleife oder Virtual ThreadsParallel Streams sind kein Ersatz für kontrollierte Concurrency.
Master-Regel: Verwende Streams für Datenfluss, nicht als Versteck für Geschäftsprozesse. Wenn du Debugging-Kommentare brauchst, extrahiere benannte Methoden.
10.1 Custom Collector für Finanzsummen pro Kunde

Zeigt Supplier, Accumulator, Combiner und Finisher mit fachlicher Rundung.

package com.seb4u.demo.streams;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collector;

public final class MoneyCollectors {
    public record Invoice(String customerId, BigDecimal net, BigDecimal tax) {}
    public record CustomerTotals(BigDecimal net, BigDecimal tax, BigDecimal gross, int count) {}

    private static final class Accumulator {
        BigDecimal net = BigDecimal.ZERO;
        BigDecimal tax = BigDecimal.ZERO;
        int count;
        void add(Invoice invoice) {
            net = net.add(invoice.net());
            tax = tax.add(invoice.tax());
            count++;
        }
        Accumulator merge(Accumulator other) {
            net = net.add(other.net);
            tax = tax.add(other.tax);
            count += other.count;
            return this;
        }
        CustomerTotals finish() {
            BigDecimal gross = net.add(tax).setScale(2, RoundingMode.HALF_EVEN);
            return new CustomerTotals(net.setScale(2, RoundingMode.HALF_EVEN),
                                      tax.setScale(2, RoundingMode.HALF_EVEN),
                                      gross,
                                      count);
        }
    }

    public static Collector<Invoice, ?, CustomerTotals> totals() {
        return Collector.of(Accumulator::new, Accumulator::add, Accumulator::merge, Accumulator::finish);
    }

    public static Map<String, CustomerTotals> totalsByCustomer(List<Invoice> invoices) {
        return invoices.stream().collect(java.util.stream.Collectors.groupingBy(Invoice::customerId, totals()));
    }
}
10.2 ETL-Pipeline mit sealed ParseResult und stabilem Grouping

Importiert Zeilen, trennt gültige und ungültige Datensätze und erzeugt einen Report statt Exceptions pro Zeile.

package com.seb4u.demo.streams;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Stream;

public final class EtlPipeline {
    public sealed interface Parsed permits ValidRow, InvalidRow { int lineNo(); }
    public record ValidRow(int lineNo, String account, long cents) implements Parsed {}
    public record InvalidRow(int lineNo, String raw, String reason) implements Parsed {}
    public record ImportReport(List<ValidRow> valid, List<InvalidRow> invalid, long totalCents) {}

    public static Parsed parse(int lineNo, String raw) {
        String[] parts = raw.split(";");
        if (parts.length != 2) return new InvalidRow(lineNo, raw, "expected account;cents");
        try {
            long cents = Long.parseLong(parts[1]);
            if (cents <= 0) return new InvalidRow(lineNo, raw, "amount must be positive");
            return new ValidRow(lineNo, parts[0].trim(), cents);
        } catch (NumberFormatException ex) {
            return new InvalidRow(lineNo, raw, "invalid cents: " + parts[1]);
        }
    }

    public static ImportReport importLines(List<String> lines) {
        List<Parsed> parsed = Stream.iterate(0, i -> i + 1)
                .limit(lines.size())
                .map(i -> parse(i + 1, lines.get(i)))
                .toList();

        List<ValidRow> valid = parsed.stream()
                .flatMap(p -> p instanceof ValidRow v ? Stream.of(v) : Stream.empty())
                .toList();
        List<InvalidRow> invalid = parsed.stream()
                .flatMap(p -> p instanceof InvalidRow e ? Stream.of(e) : Stream.empty())
                .toList();
        long total = valid.stream().mapToLong(ValidRow::cents).sum();
        return new ImportReport(valid, invalid, total);
    }

    public static <T, K> Map<K, List<T>> stableGroup(List<T> input, Function<T, K> key) {
        return input.stream().collect(
                LinkedHashMap::new,
                (map, item) -> map.computeIfAbsent(key.apply(item), ignored -> new ArrayList<>()).add(item),
                (left, right) -> right.forEach((k, v) -> left.merge(k, v, (a, b) -> { a.addAll(b); return a; }))
        );
    }
}
10.3 Batch-Stream über Iterator für große Imports

Verarbeitet große Datenquellen chunkweise, ohne die komplette Eingabe im Speicher zu halten.

package com.seb4u.demo.streams;

import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public final class BatchStreams {
    public static <T> Stream<List<T>> batches(Iterator<T> source, int batchSize) {
        if (batchSize < 1) throw new IllegalArgumentException("batchSize must be positive");
        Spliterator<List<T>> spliterator = new Spliterators.AbstractSpliterator<>(Long.MAX_VALUE, Spliterator.ORDERED) {
            @Override public boolean tryAdvance(Consumer<? super List<T>> action) {
                if (!source.hasNext()) return false;
                java.util.ArrayList<T> batch = new java.util.ArrayList<>(batchSize);
                while (source.hasNext() && batch.size() < batchSize) {
                    batch.add(source.next());
                }
                action.accept(List.copyOf(batch));
                return true;
            }
        };
        return StreamSupport.stream(spliterator, false);
    }

    public static void processLargeImport(Iterator<String> lines) {
        batches(lines, 1_000)
                .map(EtlPipeline::importLines)
                .forEach(report -> {
                    if (!report.invalid().isEmpty()) {
                        System.err.println("invalid rows: " + report.invalid().size());
                    }
                    System.out.println("posting cents: " + report.totalCents());
                });
    }
}
Anti-Pattern:

Stream-Ketten mit 20 Schritten, Seiteneffekten in peek und unklarer Fehlerbehandlung.

Besser:

Kurze Pipeline, benannte Transformationsmethoden, klare Fehlerobjekte und explizite Ressourcenverwaltung.

Vertiefung zu Kapitel 11

Deep Dive: Exceptions und robuste Fehlerverträge

Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K50

Fehlerbehandlung ist Teil des API-Designs. Gute Fehlerverträge unterscheiden Validierung, Konflikt, Nicht-gefunden, technische Störung und Programmierfehler.

Kapitel 11 Deep Dive: Fehlergrenzen Kapitel 11 Deep Dive: Fehlergrenzen Input validieren Domain Exception/Result Boundary Mapping API Error Contract Ops Log + Trace Ausnahmen sollen intern präzise sein und extern kontrolliert übersetzt werden.
FehlerartBehandlungBeispiel
Validierungsfehlersammeln und als 400/Validation Contract zurückgebenfehlende E-Mail, ungültiges Alter
Domänenkonfliktfachliche Exception mit CodeBestellung bereits erfüllt, Version veraltet
Nicht gefundenOptional intern, 404 an der API-GrenzeUserId existiert nicht
Technische StörungRetry nur wenn idempotent und retryableTimeout, temporärer Netzwerkfehler
Programmierfehlernicht verschluckenNullPointerException durch Bug, Assertion verletzt
  1. Exceptions nicht als normale Schleifensteuerung missbrauchen.
  2. Fehlercodes stabiler halten als Fehlermeldungen.
  3. Technische Details nicht ungefiltert an API-Clients geben.
  4. Bei Batch-Prozessen Fehler aggregieren, statt beim ersten Datensatz blind abzubrechen.
11.1 API-Error-Contract mit DomainException-Hierarchie

Trennt interne Fehlerklassen von externen API-Fehlerantworten.

package com.seb4u.demo.errors;

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

public final class ErrorContracts {
    public enum ErrorCode {
        VALIDATION_FAILED(400), NOT_FOUND(404), CONFLICT(409), RATE_LIMITED(429), INTERNAL(500);
        private final int httpStatus;
        ErrorCode(int httpStatus) { this.httpStatus = httpStatus; }
        public int httpStatus() { return httpStatus; }
    }

    public static abstract class DomainException extends RuntimeException {
        private final ErrorCode code;
        protected DomainException(ErrorCode code, String message) {
            super(message);
            this.code = code;
        }
        public ErrorCode code() { return code; }
    }

    public static final class ValidationException extends DomainException {
        private final Map<String, String> fieldErrors;
        public ValidationException(Map<String, String> fieldErrors) {
            super(ErrorCode.VALIDATION_FAILED, "validation failed");
            this.fieldErrors = Map.copyOf(fieldErrors);
        }
        public Map<String, String> fieldErrors() { return fieldErrors; }
    }

    public static final class ConflictException extends DomainException {
        public ConflictException(String message) { super(ErrorCode.CONFLICT, message); }
    }

    public record ApiError(String id, String code, int status, String message, Instant timestamp, Map<String, String> details) {}

    public static ApiError toApiError(Throwable throwable) {
        return switch (throwable) {
            case ValidationException ex -> new ApiError(UUID.randomUUID().toString(), ex.code().name(),
                    ex.code().httpStatus(), ex.getMessage(), Instant.now(), ex.fieldErrors());
            case DomainException ex -> new ApiError(UUID.randomUUID().toString(), ex.code().name(),
                    ex.code().httpStatus(), ex.getMessage(), Instant.now(), Map.of());
            default -> new ApiError(UUID.randomUUID().toString(), ErrorCode.INTERNAL.name(),
                    500, "internal server error", Instant.now(), Map.of("type", throwable.getClass().getName()));
        };
    }
}
11.2 Retry-Policy mit Backoff, Jitter und Exception-Filter

Retry nur für explizit erlaubte Fehlerarten; nicht für Validierungsfehler oder nicht-idempotente Operationen.

package com.seb4u.demo.errors;

import java.time.Duration;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Predicate;

public final class RetryPolicy {
    public record RetryConfig(int maxAttempts, Duration firstDelay, double multiplier) {}

    public static <T> T retry(Callable<T> operation, RetryConfig config, Predicate<Throwable> retryable) throws Exception {
        Throwable last = null;
        Duration delay = config.firstDelay();
        for (int attempt = 1; attempt <= config.maxAttempts(); attempt++) {
            try {
                return operation.call();
            } catch (Throwable ex) {
                last = ex;
                if (attempt == config.maxAttempts() || !retryable.test(ex)) break;
                sleepWithJitter(delay);
                delay = Duration.ofMillis((long) (delay.toMillis() * config.multiplier()));
            }
        }
        if (last instanceof Exception e) throw e;
        if (last instanceof Error e) throw e;
        throw new IllegalStateException(last);
    }

    private static void sleepWithJitter(Duration base) throws InterruptedException {
        long jitter = ThreadLocalRandom.current().nextLong(0, Math.max(1, base.toMillis() / 3));
        Thread.sleep(base.plusMillis(jitter));
    }

    public static Predicate<Throwable> retryOn(Class<?>... types) {
        Set<Class<?>> retryable = Set.of(types);
        return ex -> retryable.stream().anyMatch(type -> type.isAssignableFrom(ex.getClass()));
    }
}
11.3 Aggregierte Validierung und suppressed Exceptions im Batch-Import

Zeigt, wie mehrere Fehler gesammelt und trotzdem diagnostizierbar gehalten werden.

package com.seb4u.demo.errors;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public final class ValidationAggregate {
    public record UserRegistration(String email, String password, int age) {}

    public static void validate(UserRegistration form) {
        Map<String, String> errors = collectErrors(form);
        if (!errors.isEmpty()) throw new ErrorContracts.ValidationException(errors);
    }

    public static Map<String, String> collectErrors(UserRegistration form) {
        java.util.LinkedHashMap<String, String> errors = new java.util.LinkedHashMap<>();
        if (form.email() == null || !form.email().contains("@")) errors.put("email", "must be a valid email");
        if (form.password() == null || form.password().length() < 12) errors.put("password", "minimum length is 12");
        if (form.age() < 18) errors.put("age", "must be at least 18");
        return errors;
    }

    public static RuntimeException aggregate(String message, List<? extends Throwable> failures) {
        RuntimeException combined = new RuntimeException(message + " (" + failures.size() + " failures)");
        failures.forEach(combined::addSuppressed);
        return combined;
    }

    public static void importUsers(List<UserRegistration> forms) {
        List<Throwable> failures = new ArrayList<>();
        for (UserRegistration form : forms) {
            try {
                validate(form);
                // save(form)
            } catch (RuntimeException ex) {
                failures.add(ex);
            }
        }
        if (!failures.isEmpty()) throw aggregate("user import failed", failures);
    }
}
Anti-Pattern:

catch (Exception e) { return null; } löscht Ursache, Kontext und Diagnose.

Besser:

Kontext hinzufügen, passende Fehlerklasse verwenden, an Systemgrenzen übersetzen und strukturierte Logs erzeugen.

Modul C

⚙️ Runtime, I/O, Zeit & Concurrency

Dateien, NIO.2, Ressourcen, Zeit/Locale, Virtual Threads, Async-Workflows, Speicher, GC, Module und Builds.

16Themen in diesem Modul
47Codeblöcke
16SVG-Grafiken
📁

I/O, Dateien, NIO.2 und Ressourcen

Modul C · Runtime, I/O, Zeit & Concurrency · Alt-Referenz: K12

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

I/O-Pipeline I/O-Pipeline Path Files Stream/Channel Parser Domänenobjekt I/O-Code braucht Ressourcenmanagement und Fehlerstrategie.

Datei- und Netzwerkzugriffe sind fehleranfällig: Pfade fehlen, Rechte sind falsch, Encoding passt nicht, Dateien sind groß oder Prozesse brechen ab. Nutze `Path` statt roher String-Pfade und `try-with-resources`, um Streams zuverlässig zu schließen.

NIO.2 bietet `Files`, `Path`, `FileVisitor`, Watch Services und Channels. Für kleine Dateien sind `Files.readString` oder `readAllLines` bequem. Für große Dateien solltest du streaming-basiert arbeiten, damit nicht alles in den Speicher geladen wird.

Sicherheit: Akzeptiere Pfade von außen nie ungeprüft. Verhindere Path Traversal, validiere Dateiendungen nicht als alleinige Sicherheitsmaßnahme und trenne Upload-Speicher von ausführbarem Code.

Beispielcode

Path base = Path.of("uploads").toAbsolutePath().normalize();
Path requested = base.resolve("../secret.txt").normalize();
if (!requested.startsWith(base)) {
    throw new SecurityException("path_traversal");
}
try (var lines = java.nio.file.Files.lines(requested)) {
    lines.filter(line -> !line.isBlank()).forEach(System.out::println);
}

Übungen

  • Schreibe einen CSV-Reader mit `Files.lines`.
  • Implementiere Path-Traversal-Schutz.
  • Vergleiche `readString` mit Streaming bei großen Dateien.

Mastery-Check

  • Du nutzt `try-with-resources` automatisch.
  • Du unterscheidest kleine und große Dateioperationen.
  • Du validierst Pfade sicher.

Datum, Zeit, Locale und Text

Modul C · Runtime, I/O, Zeit & Concurrency · Alt-Referenz: K13

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Zeitmodell Zeitmodell Instant: maschinenlesbarer Zeitpunkt ZonedDateTime: Zeitpunkt mit Zeitzone LocalDateTime: lokale Wandzeit Duration/Period: Abstände Locale/Formatter: Darstellung Speichere Zeitpunkte eindeutig, formatiere erst an der Grenze zum Menschen.

Zeit ist schwieriger als sie aussieht. Nutze die `java.time`-API. `Instant` ist ein globaler Zeitpunkt. `LocalDate` ist ein Kalenderdatum ohne Uhrzeit. `LocalDateTime` hat keine Zeitzone und ist daher für globale Ereignisse gefährlich. `ZonedDateTime` verbindet lokale Zeit mit Zeitzone.

Speichere technische Ereignisse meist als `Instant`. Zeige Benutzern Zeiten mit ihrer Zeitzone und Locale. Verwende `Duration` für zeitbasierte Abstände und `Period` für kalenderbasierte Abstände.

Textverarbeitung verlangt Encoding-Bewusstsein. Nutze UTF-8 explizit, wenn Daten über Systemgrenzen gehen. Locale beeinflusst Sortierung, Groß-/Kleinschreibung, Dezimaltrennzeichen und Datumsformate.

Beispielcode

var now = java.time.Instant.now();
var vienna = now.atZone(java.time.ZoneId.of("Europe/Vienna"));
var formatted = java.time.format.DateTimeFormatter
    .ofPattern("dd.MM.yyyy HH:mm z", java.util.Locale.GERMANY)
    .format(vienna);
System.out.println(formatted);

Übungen

  • Konvertiere `Instant` nach drei Zeitzonen.
  • Teste Sommerzeitwechsel in `Europe/Vienna`.
  • Formatiere Währung und Datum für Deutsch und Englisch.

Mastery-Check

  • Du speicherst Zeitpunkte nicht als naive Strings.
  • Du verstehst den Unterschied zwischen Instant und LocalDateTime.
  • Du berücksichtigst Locale bei Darstellung.
🧵

Nebenläufigkeit: Threads bis Virtual Threads

Modul C · Runtime, I/O, Zeit & Concurrency · Alt-Referenz: K14

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Concurrency-Schichten Concurrency-Schichten Problem: Latenz, Parallelität, Durchsatz Abstraktion: Executor, Future, CompletableFuture Mechanik: Platform Threads und Virtual Threads Koordination: Locks, Atomics, Queues Risiken: Race Conditions, Deadlocks, Starvation Nebenläufigkeit ist Designproblem, nicht nur API-Nutzung.

Nebenläufigkeit bedeutet, dass mehrere Aufgaben zeitlich überlappend laufen. Parallelität bedeutet tatsächliche gleichzeitige Ausführung auf mehreren Kernen. Java bietet klassische Platform Threads, Executor Services, Futures, CompletableFuture, Synchronisation, Atomics, Concurrent Collections und moderne Virtual Threads.

Virtual Threads sind besonders stark für viele blockierende I/O-Aufgaben, etwa HTTP-Requests oder Datenbankzugriffe. Sie machen blockierenden Code wieder einfacher lesbar, ersetzen aber keine Datenbank-Pool-Limits, Backpressure oder fachliche Transaktionen.

Race Conditions entstehen, wenn mehrere Threads denselben veränderlichen Zustand ohne Schutz lesen und schreiben. Locks, Atomics, immutable Daten und Message Passing sind Gegenmittel. Der beste Shared State ist oft gar kein Shared State.

Beispielcode

try (var executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
    var futures = java.util.stream.IntStream.range(0, 1_000)
        .mapToObj(i -> executor.submit(() -> fetchCustomer(i)))
        .toList();
    for (var f : futures) {
        System.out.println(f.get());
    }
}

static String fetchCustomer(int id) throws Exception {
    Thread.sleep(50); // simuliert blockierendes I/O
    return "customer-" + id;
}

Übungen

  • Baue eine Race Condition mit gemeinsamem Counter.
  • Repariere sie mit `AtomicInteger` und danach mit Lock.
  • Starte 10.000 Virtual Threads mit simuliertem I/O.

Mastery-Check

  • Du unterscheidest Parallelität und Nebenläufigkeit.
  • Du kennst typische Race-Condition-Ursachen.
  • Du weißt, wann Virtual Threads sinnvoll sind.
🔗

CompletableFuture, Timeouts und asynchrone Workflows

Modul C · Runtime, I/O, Zeit & Concurrency · Alt-Referenz: K15

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Asynchroner Workflow Asynchroner Workflow API Request Response Service Validate Compose Map Remote A Call A Result A Remote B Call B Result B Asynchronität braucht Timeouts, Fehlerpfade und klare Zusammenführung.

`CompletableFuture` modelliert zukünftige Ergebnisse und erlaubt Transformationen, Kombinationen und Fehlerbehandlung. Es ist nützlich, wenn unabhängige Operationen parallel laufen können. Der Code wird jedoch schnell schwer lesbar, wenn Fehlerpfade, Timeouts und Executor-Wahl ignoriert werden.

Vermeide implizite Nutzung des Common ForkJoinPools für blockierende Operationen. Gib einen passenden Executor an. Setze Timeouts, sonst können hängende Abhängigkeiten deine Anwendung blockieren.

Masterregel: Jede asynchrone Operation braucht Antwort auf drei Fragen: Was passiert bei Erfolg? Was passiert bei Fehler? Was passiert bei Langsamkeit?

Beispielcode

CompletableFuture<Customer> customer = CompletableFuture.supplyAsync(() -> loadCustomer(id), ioExecutor);
CompletableFuture<List<Order>> orders = CompletableFuture.supplyAsync(() -> loadOrders(id), ioExecutor);

CompletableFuture<CustomerView> view = customer.thenCombine(orders, CustomerView::new)
    .orTimeout(800, java.util.concurrent.TimeUnit.MILLISECONDS)
    .exceptionally(ex -> CustomerView.fallback(id));

Übungen

  • Kombiniere zwei Futures mit `thenCombine`.
  • Füge Timeout und Fallback hinzu.
  • Vergleiche Lesbarkeit mit Virtual-Thread-Code.

Mastery-Check

  • Du vergisst Timeouts nicht.
  • Du kennst `thenApply`, `thenCompose`, `thenCombine`.
  • Du wählst Executor bewusst.
🧠

Speicher, Stack, Heap und Garbage Collection

Modul C · Runtime, I/O, Zeit & Concurrency · Alt-Referenz: K16

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

JVM-Speicherbild JVM-Speicherbild Thread Stack: lokale Variablen, Frames Heap: Objekte und Arrays Metaspace: Klassendaten Code Cache: JIT-kompilierter Code Native Memory: OS/JNI/Direct Buffers Speicherprobleme sind oft Lebensdauerprobleme.

Jede Methode erzeugt Stack Frames für lokale Variablen und Aufrufinformationen. Objekte liegen im Heap. Klassenmetadaten liegen im Metaspace. Die Garbage Collection entfernt Objekte, die nicht mehr erreichbar sind. Ein Memory Leak in Java bedeutet meist: Objekte sind fachlich nicht mehr gebraucht, aber noch erreichbar.

Typische Leak-Quellen sind statische Maps, Caches ohne Grenzen, Listener ohne Deregistrierung, ThreadLocals, ungeschlossene Ressourcen und zu große Objektgraphen in Sessions.

Garbage Collector Tuning sollte nicht der erste Schritt sein. Zuerst misst du Allokationsrate, Objektlebensdauer, Heap-Dumps und Latenzanforderungen. Dann optimierst du Datenstrukturen, Lebensdauer und Caches.

Beispielcode

// Typischer Leak-Geruch: unbeschränkter Cache
static final Map<String, byte[]> CACHE = new java.util.concurrent.ConcurrentHashMap<>();

static byte[] load(String key) {
    return CACHE.computeIfAbsent(key, k -> new byte[1_000_000]);
}
// Besser: Größe, TTL, Metriken und klare Invalidierung definieren.

Übungen

  • Erzeuge absichtlich hohen Speicherverbrauch und beobachte `jcmd`.
  • Schreibe einen begrenzten Cache.
  • Analysiere, warum statische Collections gefährlich sein können.

Mastery-Check

  • Du kannst Stack und Heap erklären.
  • Du erkennst Java Memory Leaks.
  • Du misst vor GC-Tuning.
📦

Module, Packages und Architekturgrenzen

Modul C · Runtime, I/O, Zeit & Concurrency · Alt-Referenz: K17

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Modulgraph Modulgraph app domain orders customers application use cases ports infrastructure jdbc http api controllers dto Gute Paketstruktur zeigt Architektur, nicht nur technische Schichten.

Packages organisieren Code und Sichtbarkeit. Module können stärkere Grenzen definieren: Welche Packages exportiert werden und welche Abhängigkeiten erlaubt sind. Auch ohne Java Platform Module System solltest du Architekturgrenzen ernst nehmen.

Eine gute Struktur trennt Domäne, Anwendungsfälle, Infrastruktur und Schnittstellen. Domänencode sollte nicht von HTTP, SQL oder Frameworkdetails abhängen. Ports und Adapter helfen, fachlichen Kern testbar zu halten.

Masterblick: Eine Architektur ist gut, wenn falsche Abhängigkeiten schwer werden. Wenn jede Klasse überall alles importieren kann, ist Architektur nur eine Zeichnung.

Beispielcode

// module-info.java
module com.seb4u.demo.shop {
    exports com.seb4u.demo.shop.api;
    exports com.seb4u.demo.shop.domain;
    requires java.sql;
}

Übungen

  • Zeichne die Pakete deines Projekts als Abhängigkeitsgraph.
  • Verhindere, dass Domain-Code Infrastruktur importiert.
  • Teste `jdeps` gegen dein Build-Artefakt.

Mastery-Check

  • Du nutzt Packages als Architekturwerkzeug.
  • Du kennst Grundidee von `module-info.java`.
  • Du trennst Domäne von Infrastruktur.
🏗️

Build, Dependencies und reproduzierbare Projekte

Modul C · Runtime, I/O, Zeit & Concurrency · Alt-Referenz: K18

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Build-Lifecycle Build-Lifecycle validate compile test package verify deploy Ein Build ist ein automatisierter Qualitätsvertrag.

Ein Build kompiliert nicht nur Code. Er löst Abhängigkeiten, führt Tests aus, prüft Stil, erzeugt Artefakte, scannt Schwachstellen und veröffentlicht Ergebnisse. Maven und Gradle sind die verbreiteten Werkzeuge. Wichtig ist weniger, welches Tool du wählst, sondern ob Builds reproduzierbar, schnell und verständlich sind.

Fixiere Versionen, vermeide zufällige Abhängigkeitsupdates und dokumentiere erforderliche JDK-Versionen. Ein Projekt sollte nach `git clone` mit einem klaren Befehl laufen.

Masterregel: Was nicht automatisiert im Build geprüft wird, ist nur Hoffnung.

Beispielcode

<!-- pom.xml Ausschnitt -->
<properties>
  <maven.compiler.release>25</maven.compiler.release>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
    </plugin>
  </plugins>
</build>

Übungen

  • Erstelle ein Maven- oder Gradle-Projekt.
  • Füge Unit Tests in den Build ein.
  • Erzeuge ein ausführbares JAR.

Mastery-Check

  • Du kannst ein Projekt reproduzierbar bauen.
  • Du trennst Compile-, Test- und Runtime-Abhängigkeiten.
  • Du nutzt den Build als Qualitätsgate.
📁
Professional Deep Dive

Java I/O & NIO.2 – Produktionsreife Datei- und Datenverarbeitung

Modul C · Runtime, I/O, Zeit & Concurrency · Alt-Referenz: K44

Mehr Detail, mehr Architekturdenken, mehr komplexer Beispielcode für robuste I/O-Systeme.

8zusätzliche I/O-Code-Labs
Streamingstatt Heap-Explosion
Atomicschreiben ohne kaputte Dateien
SecurePath Traversal und Grenzen

Professionelles Java-I/O bedeutet nicht nur Dateien lesen und schreiben. Es bedeutet: Ressourcen sicher schließen, Encodings explizit wählen, Speicherverbrauch begrenzen, Dateisystemfehler erwarten, Pfade gegen Angriffe härten, große Datenmengen streaming-basiert verarbeiten und Schreibvorgänge so bauen, dass ein Prozessabbruch keine halb fertigen Dateien hinterlässt.

Die wichtigste Denkregel lautet: I/O ist eine Systemgrenze. Alles, was über diese Grenze kommt, ist unzuverlässig: Dateien können fehlen, Berechtigungen können sich ändern, Daten können korrupt sein, ein Mount kann langsam werden, eine Datei kann während des Lesens verschwinden, und Nutzerpfade können absichtlich manipuliert sein.

I/O als Pipeline: validieren · streamen · transformieren · atomar veröffentlichen Path Boundary normalize() startsWith(root) Streaming BufferedReader Channels Transform Parser Validator Atomic Write temp file move replace Fehlervertrag Recoverable: retry · skip · quarantine Fatal: fail fast + Diagnose Prinzip: Nicht alles in den Heap laden — Daten fließen kontrolliert durch kleine, messbare Schritte.

1. Boundary zuerst

Externe Pfade nie direkt verwenden. Immer normalisieren, gegen ein Root-Verzeichnis prüfen und Dateigrößen/Typen begrenzen.

2. Streaming bevorzugen

Große Dateien mit Readern, Streams, Channels oder FileVisitor verarbeiten. readAllBytes nur für kleine, begrenzte Dateien.

3. Atomare Ausgabe

Erst in eine temporäre Datei schreiben, dann mit move ersetzen. So bleibt das Ziel bei Fehlern konsistent.

4. Diagnosefähigkeit

Zeilennummern, Quarantine-Dateien und strukturierte Fehler helfen, Datenprobleme schnell zu finden.

SituationGute WahlRisiko bei schlechter Wahl
Kleine TextdateiFiles.readString mit Charset und GrößenlimitUnbegrenzt lesen, falsches Encoding, OutOfMemory
Große Log-/CSV-DateiBufferedReader, Zeile für Zeile, Fehler mit ZeilennummerAlles in Liste laden, Fehlerkontext verlieren
Viele DateienFiles.walkFileTree mit FehlerbehandlungSymlinks/Rechte ignorieren, Abbruch beim ersten Fehler
Export/ReportTemp-Datei + atomarer MoveHalbe Datei nach Crash sichtbar
Upload/BenutzerpfadRoot-Sandbox, Normalize, Whitelist, GrößenlimitPath Traversal, Datenleak, Überschreiben fremder Dateien
Master-Regel: Schreibe I/O-Code so, als ob jeder Zugriff fehlschlagen kann. Guter Code macht diese Fehler sichtbar, begrenzt den Schaden und bleibt testbar.

Komplexe I/O-Beispielcodes

Sichere Pfad-Auflösung gegen Path Traversal

Professionelles Muster: klare Grenze, expliziter Fehlervertrag, keine unnötige Heap-Last und testbare Einheiten.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;

public final class SafePathResolver {
    private final Path root;
    private final Set<String> allowedExtensions;

    public SafePathResolver(Path root, Set<String> allowedExtensions) throws IOException {
        this.root = root.toRealPath();
        this.allowedExtensions = Set.copyOf(allowedExtensions);
    }

    public Path resolveUserFile(String userInput) throws IOException {
        if (userInput == null || userInput.isBlank()) {
            throw new IllegalArgumentException("Dateiname fehlt");
        }
        Path candidate = root.resolve(userInput).normalize();
        if (!candidate.startsWith(root)) {
            throw new SecurityException("Path Traversal blockiert: " + userInput);
        }
        String fileName = candidate.getFileName().toString();
        int dot = fileName.lastIndexOf('.');
        String ext = dot >= 0 ? fileName.substring(dot + 1).toLowerCase() : "";
        if (!allowedExtensions.contains(ext)) {
            throw new SecurityException("Dateityp nicht erlaubt: " + ext);
        }
        return candidate;
    }

    public byte[] readSmallFile(String userInput, int maxBytes) throws IOException {
        Path file = resolveUserFile(userInput);
        long size = Files.size(file);
        if (size > maxBytes) {
            throw new IOException("Datei zu groß: " + size + " Bytes");
        }
        return Files.readAllBytes(file);
    }
}
Atomar schreiben: Temp-Datei, fsync-nahe Semantik, Replace

Professionelles Muster: klare Grenze, expliziter Fehlervertrag, keine unnötige Heap-Last und testbare Einheiten.

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.UUID;

public final class AtomicFileWriter {
    public static void writeLinesAtomically(Path target, List<String> lines) throws IOException {
        Path dir = target.toAbsolutePath().getParent();
        Files.createDirectories(dir);
        Path tmp = dir.resolve(target.getFileName() + "." + UUID.randomUUID() + ".tmp");
        boolean success = false;
        try {
            try (BufferedWriter writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8)) {
                for (String line : lines) {
                    writer.write(line);
                    writer.newLine();
                }
                writer.flush();
            }
            Files.move(tmp, target,
                    StandardCopyOption.ATOMIC_MOVE,
                    StandardCopyOption.REPLACE_EXISTING);
            success = true;
        } finally {
            if (!success) {
                Files.deleteIfExists(tmp);
            }
        }
    }
}
Große CSV-Datei streaming-basiert verarbeiten

Professionelles Muster: klare Grenze, expliziter Fehlervertrag, keine unnötige Heap-Last und testbare Einheiten.

import java.io.BufferedReader;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;

record Sale(String region, String product, BigDecimal amount) {}

public final class StreamingCsvAggregator {
    public Map<String, BigDecimal> sumByRegion(Path csv) throws IOException {
        Map<String, BigDecimal> totals = new HashMap<>();
        try (BufferedReader reader = Files.newBufferedReader(csv, StandardCharsets.UTF_8)) {
            String header = reader.readLine();
            if (header == null || !header.equals("region,product,amount")) {
                throw new IOException("Unerwarteter CSV-Header");
            }
            String line;
            long lineNo = 1;
            while ((line = reader.readLine()) != null) {
                lineNo++;
                try {
                    Sale sale = parse(line);
                    totals.merge(sale.region(), sale.amount(), BigDecimal::add);
                } catch (RuntimeException ex) {
                    throw new IOException("Fehler in Zeile " + lineNo + ": " + line, ex);
                }
            }
        }
        return totals;
    }

    private Sale parse(String line) {
        String[] parts = line.split(",", -1);
        if (parts.length != 3) throw new IllegalArgumentException("Spaltenanzahl falsch");
        return new Sale(parts[0].trim(), parts[1].trim(), new BigDecimal(parts[2].trim()));
    }
}
NIO FileVisitor: Verzeichnisbaum robust indizieren

Professionelles Muster: klare Grenze, expliziter Fehlervertrag, keine unnötige Heap-Last und testbare Einheiten.

import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;

record FileFingerprint(Path path, long size, String sha256) {}

public final class DirectoryIndexer {
    public List<FileFingerprint> index(Path root) throws IOException {
        List<FileFingerprint> result = new ArrayList<>();
        Files.walkFileTree(root, new SimpleFileVisitor<>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                if (attrs.isRegularFile()) {
                    result.add(new FileFingerprint(root.relativize(file), attrs.size(), sha256(file)));
                }
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(Path file, IOException exc) {
                System.err.println("Überspringe nicht lesbare Datei: " + file + " -> " + exc.getMessage());
                return FileVisitResult.CONTINUE;
            }
        });
        return result;
    }

    private String sha256(Path file) throws IOException {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            try (var in = Files.newInputStream(file)) {
                in.transferTo(new java.io.OutputStream() {
                    @Override public void write(int b) { md.update((byte) b); }
                    @Override public void write(byte[] b, int off, int len) { md.update(b, off, len); }
                });
            }
            return HexFormat.of().formatHex(md.digest());
        } catch (Exception e) {
            throw new IOException("Hashing fehlgeschlagen", e);
        }
    }
}
WatchService mit Debounce für Hot-Reload/Import-Verzeichnisse

Professionelles Muster: klare Grenze, expliziter Fehlervertrag, keine unnötige Heap-Last und testbare Einheiten.

import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public final class DebouncedDirectoryWatcher implements AutoCloseable {
    private final WatchService watchService;
    private final ScheduledExecutorService debounce = Executors.newSingleThreadScheduledExecutor();
    private final Map<Path, Long> pending = new ConcurrentHashMap<>();
    private volatile boolean running = true;

    public DebouncedDirectoryWatcher(Path dir) throws IOException {
        this.watchService = dir.getFileSystem().newWatchService();
        dir.register(watchService,
                StandardWatchEventKinds.ENTRY_CREATE,
                StandardWatchEventKinds.ENTRY_MODIFY,
                StandardWatchEventKinds.ENTRY_DELETE);
    }

    public void start(Path dir, java.util.function.Consumer<Path> onStableChange) {
        debounce.scheduleWithFixedDelay(() -> flushStable(onStableChange), 250, 250, TimeUnit.MILLISECONDS);
        Thread.ofVirtual().name("watcher").start(() -> {
            while (running) {
                try {
                    WatchKey key = watchService.take();
                    for (WatchEvent<?> event : key.pollEvents()) {
                        if (event.kind() == StandardWatchEventKinds.OVERFLOW) continue;
                        Path changed = dir.resolve((Path) event.context()).normalize();
                        pending.put(changed, System.nanoTime());
                    }
                    if (!key.reset()) break;
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                } catch (ClosedWatchServiceException closed) {
                    break;
                }
            }
        });
    }

    private void flushStable(java.util.function.Consumer<Path> onStableChange) {
        long now = System.nanoTime();
        pending.entrySet().removeIf(entry -> {
            boolean stable = TimeUnit.NANOSECONDS.toMillis(now - entry.getValue()) >= 700;
            if (stable) onStableChange.accept(entry.getKey());
            return stable;
        });
    }

    @Override public void close() throws IOException {
        running = false;
        debounce.shutdownNow();
        watchService.close();
    }
}
Zero-Copy-nahe Dateiübertragung mit FileChannel.transferTo

Professionelles Muster: klare Grenze, expliziter Fehlervertrag, keine unnötige Heap-Last und testbare Einheiten.

import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public final class ChannelCopier {
    public static long copy(Path source, Path target) throws IOException {
        try (FileChannel in = FileChannel.open(source, StandardOpenOption.READ);
             FileChannel out = FileChannel.open(target,
                     StandardOpenOption.CREATE,
                     StandardOpenOption.TRUNCATE_EXISTING,
                     StandardOpenOption.WRITE)) {
            long size = in.size();
            long transferred = 0;
            while (transferred < size) {
                long n = in.transferTo(transferred, size - transferred, out);
                if (n <= 0) break; // verhindert Endlosschleifen bei speziellen Filesystemen
                transferred += n;
            }
            return transferred;
        }
    }
}
Retry-fähiger Export mit Quarantine-Verzeichnis

Professionelles Muster: klare Grenze, expliziter Fehlervertrag, keine unnötige Heap-Last und testbare Einheiten.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.List;

record ExportJob(String name, List<String> lines) {}

public final class ResilientExporter {
    private final Path outbox;
    private final Path quarantine;

    public ResilientExporter(Path outbox, Path quarantine) {
        this.outbox = outbox;
        this.quarantine = quarantine;
    }

    public void export(ExportJob job) throws IOException {
        Files.createDirectories(outbox);
        Files.createDirectories(quarantine);
        Path target = outbox.resolve(job.name() + ".txt");
        try {
            AtomicFileWriter.writeLinesAtomically(target, job.lines());
        } catch (IOException ex) {
            Path diagnostic = quarantine.resolve(job.name() + "-" + Instant.now().toEpochMilli() + ".failed.txt");
            Files.writeString(diagnostic, "Export fehlgeschlagen: " + ex.getMessage());
            throw ex;
        }
    }
}
Memory-Mapped Read für große, read-only Indexdateien

Professionelles Muster: klare Grenze, expliziter Fehlervertrag, keine unnötige Heap-Last und testbare Einheiten.

import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public final class MappedPrefixReader {
    public static String readPrefix(Path file, int maxBytes) throws IOException {
        try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) {
            long size = Math.min(channel.size(), maxBytes);
            MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, size);
            byte[] bytes = new byte[(int) size];
            buffer.get(bytes);
            return new String(bytes, StandardCharsets.UTF_8);
        }
    }
}

Mastery-Checkliste für I/O

  • Du kannst erklären, wann Path, Files, BufferedReader, FileChannel und WatchService sinnvoll sind.
  • Du schreibst keine Benutzerpfade ungeprüft ins Dateisystem.
  • Du kannst große Dateien verarbeiten, ohne sie vollständig in den Heap zu laden.
  • Du kannst atomare Datei-Updates bauen und bei Fehlern temporäre Dateien bereinigen.
  • Du lieferst bei Datenfehlern konkrete Diagnose: Datei, Zeile, Spalte, Ursache.
🧵
Professional Deep Dive

Java Concurrency – Virtual Threads, Backpressure, Locks und Diagnose

Modul C · Runtime, I/O, Zeit & Concurrency · Alt-Referenz: K45

Mehr Detail und produktionsnahe Beispielcodes für sichere, begrenzte und beobachtbare Parallelität.

10zusätzliche Concurrency-Code-Labs
BoundedParallelität bewusst begrenzen
CancelTimeouts und Interrupts respektieren
ObserveMetriken und Deadlock-Diagnose

Concurrency ist ein Architekturthema. Mehr Threads machen ein System nicht automatisch schneller. Professionelle Nebenläufigkeit beginnt mit der Frage: Welche Arbeit darf parallel laufen, welche Ressourcen sind begrenzt, wie wird abgebrochen, wie verhindern wir Überlast, und wie erkennen wir Fehler im Betrieb?

Virtual Threads machen blockierenden I/O-Code deutlich skalierbarer, ersetzen aber keine Architekturgrenzen. Datenbanken, APIs, Dateisysteme und externe Dienste brauchen weiterhin Limits, Timeouts, Bulkheads, Queues, Retry-Budgets und gute Diagnose.

Concurrency Master-Modell: Aufgaben, Grenzen, Koordination, Beobachtung Task Design klein · abbrechbar Timeouts · Context Executor Boundary Virtual Threads Bulkhead Shared State immutable first locks/atomics Backpressure & Recovery Queue-Limits · CallerRuns · Retry-Budget Metriken · Thread Dumps · Deadlock-Diagnose Golden Rule Concurrency ist ein Architekturthema, nicht nur ein Thread-API-Thema.

1. Immutable first

Teile möglichst unveränderliche Daten. Gemeinsamer veränderlicher Zustand ist die Hauptquelle für Race Conditions.

2. Bound everything

Begrenze Queues, parallele Calls, Retry-Anzahl und Wartezeiten. Unbegrenzte Parallelität ist versteckte Instabilität.

3. Interrupts respektieren

Fange InterruptedException nicht leer ab. Setze den Interrupt-Status wieder und beende kooperativ.

4. Diagnose einbauen

Thread-Namen, Metriken, Timeouts und Deadlock-Checks sparen im Betrieb Stunden.

ProblemWerkzeugProfessioneller Hinweis
Viele blockierende I/O-AufgabenVirtual ThreadsIdeal für Request-per-task, aber externe Ressourcen weiterhin limitieren.
Begrenzte RessourceSemaphore/BulkheadSchützt Datenbank, API, Dateisystem oder Legacy-Service.
Produzent schneller als KonsumentBounded QueueBackpressure sichtbar machen statt Speicher wachsen lassen.
Gemeinsamer ZustandImmutable, Atomic, LockErst Zustand vermeiden, dann minimale Synchronisation wählen.
Asynchrone AggregationCompletableFutureTimeouts, Fallbacks und Exception-Pfade explizit modellieren.
Master-Regel: Jede parallele Architektur braucht eine klare Antwort auf vier Fragen: Wie viele Tasks dürfen laufen? Wie lange dürfen sie warten? Wie brechen sie ab? Wie sieht man Fehler?

Komplexe Concurrency-Beispielcodes

Virtual-Thread Task Scope mit Timeout und sauberem Shutdown

Professionelles Muster: begrenzte Parallelität, sauberes Abbrechen, Timeouts, Backpressure und Diagnosefähigkeit.

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;

public final class VirtualThreadTaskScope implements AutoCloseable {
    private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
    private final List<Future<?>> futures = new ArrayList<>();

    public <T> Future<T> fork(Callable<T> task) {
        Future<T> future = executor.submit(task);
        futures.add(future);
        return future;
    }

    public void joinAll(Duration timeout) throws InterruptedException, TimeoutException, ExecutionException {
        long deadline = System.nanoTime() + timeout.toNanos();
        for (Future<?> future : futures) {
            long remaining = deadline - System.nanoTime();
            if (remaining <= 0) throw new TimeoutException("Task Scope Timeout");
            future.get(remaining, java.util.concurrent.TimeUnit.NANOSECONDS);
        }
    }

    @Override public void close() {
        executor.shutdownNow();
    }
}
Bulkhead für externe APIs: fair, limitiert, messbar

Professionelles Muster: begrenzte Parallelität, sauberes Abbrechen, Timeouts, Backpressure und Diagnosefähigkeit.

import java.time.Duration;
import java.util.concurrent.Callable;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.LongAdder;

public final class Bulkhead {
    private final Semaphore permits;
    private final LongAdder accepted = new LongAdder();
    private final LongAdder rejected = new LongAdder();

    public Bulkhead(int maxConcurrent) {
        this.permits = new Semaphore(maxConcurrent, true);
    }

    public <T> T call(Duration waitBudget, Callable<T> operation) throws Exception {
        boolean acquired = permits.tryAcquire(waitBudget.toMillis(), TimeUnit.MILLISECONDS);
        if (!acquired) {
            rejected.increment();
            throw new TimeoutException("Bulkhead voll");
        }
        accepted.increment();
        try {
            return operation.call();
        } finally {
            permits.release();
        }
    }

    public String metrics() {
        return "bulkhead.accepted=" + accepted.sum() + " bulkhead.rejected=" + rejected.sum();
    }
}
Producer/Consumer mit BlockingQueue, Poison Pill und Backpressure

Professionelles Muster: begrenzte Parallelität, sauberes Abbrechen, Timeouts, Backpressure und Diagnosefähigkeit.

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

sealed interface WorkItem permits Message, Stop {}
record Message(long id, String payload) implements WorkItem {}
record Stop() implements WorkItem {}

public final class BoundedWorkPipeline {
    private final BlockingQueue<WorkItem> queue = new ArrayBlockingQueue<>(1_000);

    public void produce(Message message) throws InterruptedException {
        boolean accepted = queue.offer(message, 200, TimeUnit.MILLISECONDS);
        if (!accepted) throw new IllegalStateException("Backpressure: Queue voll");
    }

    public void stop() throws InterruptedException {
        queue.put(new Stop());
    }

    public void consume(java.util.function.Consumer<Message> handler) throws InterruptedException {
        while (true) {
            WorkItem item = queue.take();
            switch (item) {
                case Stop ignored -> { return; }
                case Message message -> handler.accept(message);
            }
        }
    }
}
CompletableFuture-Orchestrierung mit Timeout, Fallback und Combine

Professionelles Muster: begrenzte Parallelität, sauberes Abbrechen, Timeouts, Backpressure und Diagnosefähigkeit.

import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

record Customer(String id, String tier) {}
record RiskScore(String customerId, int score) {}
record Decision(String customerId, boolean approved, String reason) {}

public final class AsyncDecisionService {
    private final Executor io = Executors.newVirtualThreadPerTaskExecutor();

    public CompletableFuture<Decision> decide(String customerId) {
        CompletableFuture<Customer> customer = CompletableFuture
                .supplyAsync(() -> loadCustomer(customerId), io)
                .orTimeout(700, java.util.concurrent.TimeUnit.MILLISECONDS);

        CompletableFuture<RiskScore> risk = CompletableFuture
                .supplyAsync(() -> loadRisk(customerId), io)
                .completeOnTimeout(new RiskScore(customerId, 80), 500, java.util.concurrent.TimeUnit.MILLISECONDS);

        return customer.thenCombine(risk, this::combine)
                .exceptionally(ex -> new Decision(customerId, false, "Technische Prüfung fehlgeschlagen"));
    }

    private Decision combine(Customer c, RiskScore r) {
        boolean approved = r.score() < 70 || c.tier().equals("VIP");
        return new Decision(c.id(), approved, approved ? "OK" : "Risiko zu hoch");
    }

    private Customer loadCustomer(String id) { return new Customer(id, "STANDARD"); }
    private RiskScore loadRisk(String id) { return new RiskScore(id, 42); }
}
Thread-safe Cache mit StampedLock und Ablaufzeit

Professionelles Muster: begrenzte Parallelität, sauberes Abbrechen, Timeouts, Backpressure und Diagnosefähigkeit.

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.StampedLock;
import java.util.function.Supplier;

record CacheEntry<V>(V value, Instant expiresAt) {}

public final class ExpiringCache<K, V> {
    private final Map<K, CacheEntry<V>> data = new HashMap<>();
    private final StampedLock lock = new StampedLock();
    private final Duration ttl;
    private final Clock clock;

    public ExpiringCache(Duration ttl, Clock clock) {
        this.ttl = ttl;
        this.clock = clock;
    }

    public V getOrLoad(K key, Supplier<V> loader) {
        Instant now = clock.instant();
        long stamp = lock.tryOptimisticRead();
        CacheEntry<V> entry = data.get(key);
        if (!lock.validate(stamp)) {
            stamp = lock.readLock();
            try { entry = data.get(key); }
            finally { lock.unlockRead(stamp); }
        }
        if (entry != null && entry.expiresAt().isAfter(now)) return entry.value();

        long write = lock.writeLock();
        try {
            CacheEntry<V> current = data.get(key);
            if (current != null && current.expiresAt().isAfter(now)) return current.value();
            V value = loader.get();
            data.put(key, new CacheEntry<>(value, now.plus(ttl)));
            return value;
        } finally {
            lock.unlockWrite(write);
        }
    }
}
AtomicReference für lock-free Konfigurations-Snapshot

Professionelles Muster: begrenzte Parallelität, sauberes Abbrechen, Timeouts, Backpressure und Diagnosefähigkeit.

import java.time.Instant;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

record RuntimeConfig(int maxRetries, int timeoutMs, Map<String, String> flags, Instant loadedAt) {}

public final class ConfigRegistry {
    private final AtomicReference<RuntimeConfig> current = new AtomicReference<>(
            new RuntimeConfig(2, 500, Map.of(), Instant.EPOCH));

    public RuntimeConfig get() {
        return current.get();
    }

    public void reload(RuntimeConfig next) {
        if (next.maxRetries() < 0 || next.timeoutMs() <= 0) {
            throw new IllegalArgumentException("Ungültige Konfiguration");
        }
        current.set(new RuntimeConfig(
                next.maxRetries(),
                next.timeoutMs(),
                Map.copyOf(next.flags()),
                Instant.now()));
    }

    public boolean isEnabled(String flag) {
        return Boolean.parseBoolean(current.get().flags().getOrDefault(flag, "false"));
    }
}
Deadlock-Diagnose mit ThreadMXBean

Professionelles Muster: begrenzte Parallelität, sauberes Abbrechen, Timeouts, Backpressure und Diagnosefähigkeit.

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.Arrays;
import java.util.stream.Collectors;

public final class DeadlockDiagnostics {
    public static String detectDeadlocks() {
        ThreadMXBean bean = ManagementFactory.getThreadMXBean();
        long[] ids = bean.findDeadlockedThreads();
        if (ids == null || ids.length == 0) {
            return "Keine Deadlocks erkannt";
        }
        ThreadInfo[] infos = bean.getThreadInfo(ids, true, true);
        return Arrays.stream(infos)
                .map(info -> "Thread " + info.getThreadName()
                        + " wartet auf " + info.getLockName()
                        + " gehalten von " + info.getLockOwnerName())
                .collect(Collectors.joining(System.lineSeparator()));
    }
}
Parallelisierung mit Rate Limit und Retry-Budget

Professionelles Muster: begrenzte Parallelität, sauberes Abbrechen, Timeouts, Backpressure und Diagnosefähigkeit.

import java.time.Duration;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public final class ParallelImporter {
    private final Semaphore rateLimit;

    public ParallelImporter(int requestsPerWindow) {
        this.rateLimit = new Semaphore(requestsPerWindow);
    }

    public <T> List<T> importAll(List<String> ids, java.util.function.Function<String, T> loader) throws InterruptedException {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<java.util.concurrent.Future<T>> futures = ids.stream()
                    .map(id -> executor.submit(() -> withRetry(() -> {
                        acquireRateLimit(Duration.ofSeconds(1));
                        return loader.apply(id);
                    }, 3)))
                    .toList();
            java.util.ArrayList<T> result = new java.util.ArrayList<>();
            for (var future : futures) {
                try { result.add(future.get()); }
                catch (Exception e) { throw new RuntimeException("Import fehlgeschlagen", e); }
            }
            return result;
        }
    }

    private void acquireRateLimit(Duration maxWait) throws InterruptedException {
        if (!rateLimit.tryAcquire(maxWait.toMillis(), TimeUnit.MILLISECONDS)) {
            throw new IllegalStateException("Rate Limit erreicht");
        }
        Thread.ofVirtual().start(() -> {
            try { Thread.sleep(1_000); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); }
            finally { rateLimit.release(); }
        });
    }

    private <T> T withRetry(java.util.concurrent.Callable<T> action, int attempts) throws Exception {
        Exception last = null;
        for (int i = 1; i <= attempts; i++) {
            try { return action.call(); }
            catch (Exception e) {
                last = e;
                Thread.sleep(100L * i * i);
            }
        }
        throw last;
    }
}
Cooperative Cancellation über Flag + Interrupt

Professionelles Muster: begrenzte Parallelität, sauberes Abbrechen, Timeouts, Backpressure und Diagnosefähigkeit.

import java.time.Duration;
import java.util.concurrent.atomic.AtomicBoolean;

public final class CancellableJob implements AutoCloseable {
    private final AtomicBoolean cancelled = new AtomicBoolean(false);
    private Thread worker;

    public void start() {
        worker = Thread.ofVirtual().name("cancellable-import").start(() -> {
            while (!cancelled.get()) {
                if (Thread.currentThread().isInterrupted()) break;
                processBatch();
            }
            cleanup();
        });
    }

    public void cancel(Duration grace) throws InterruptedException {
        cancelled.set(true);
        if (worker != null) {
            worker.interrupt();
            worker.join(grace.toMillis());
        }
    }

    private void processBatch() {
        try {
            Thread.sleep(50);
            System.out.println("Batch verarbeitet");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    private void cleanup() { System.out.println("Ressourcen freigegeben"); }

    @Override public void close() throws Exception { cancel(Duration.ofSeconds(2)); }
}
CompletableFuture Result Wrapper statt Exception-Verlust

Professionelles Muster: begrenzte Parallelität, sauberes Abbrechen, Timeouts, Backpressure und Diagnosefähigkeit.

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

sealed interface Outcome<T> permits Success, Failure {}
record Success<T>(T value) implements Outcome<T> {}
record Failure<T>(String id, Throwable error) implements Outcome<T> {}

public final class SafeAsyncBatch {
    private final Executor executor = Executors.newVirtualThreadPerTaskExecutor();

    public <T> CompletableFuture<List<Outcome<T>>> run(List<String> ids, java.util.function.Function<String, T> loader) {
        List<CompletableFuture<Outcome<T>>> futures = ids.stream()
                .map(id -> CompletableFuture.<Outcome<T>>supplyAsync(() -> new Success<>(loader.apply(id)), executor)
                        .exceptionally(ex -> new Failure<>(id, ex)))
                .toList();
        return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
                .thenApply(v -> futures.stream().map(CompletableFuture::join).toList());
    }
}

Mastery-Checkliste für Concurrency

  • Du kannst Race Condition, Deadlock, Starvation, Livelock und Backpressure unterscheiden.
  • Du begrenzt parallele Zugriffe auf Datenbanken, APIs und andere knappe Ressourcen.
  • Du verwendest Virtual Threads für blockierenden I/O-Code, aber nicht als Ausrede für unlimitierte Last.
  • Du kennst den Unterschied zwischen synchronized, Lock, StampedLock, AtomicReference und BlockingQueue.
  • Du behandelst Timeouts, Cancellation und Exceptions als Teil des Designs, nicht als nachträgliche Fehlerbehandlung.

Deep Dive: I/O, NIO.2 und Ressourcen – Enterprise-Dateipipelines

Modul C · Runtime, I/O, Zeit & Concurrency · Alt-Referenz: K51

Was du hier meisterst

Dieses Kapitel ergänzt den vorhandenen I/O-Deep-Dive um reale Produktionsmuster: sichere Importzonen, atomare Datei-Übergaben, Quarantäne, Checksums, Streaming-Verarbeitung, idempotente Verarbeitung und wiederholbare Fehleranalyse.

3komplexe Code-Labs
NIO.2Path, Files, FileChannel
ProdQuarantine + Audit
Enterprise-I/O-Pipeline: sicher, atomar, beobachtbar InboxungtrustedNormalizePath + EncodingValidateSchema + HashProcessStreamingArchiveatomic move
Sicherer PfadAlle externen Dateinamen werden gegen eine Root-Zone normalisiert und geprüft.
Streaming statt LadenGroße Dateien werden zeilenweise oder chunkweise verarbeitet.
Atomare ÜbergabeProducer schreiben temporär und verschieben dann atomar in die Inbox.
QuarantäneFehlerhafte Dateien bleiben analysierbar, ohne die Pipeline zu blockieren.
EntscheidungNimm dasVermeide das
Kleine KonfigurationFiles.readString mit explizitem CharsetDefault-Encoding und rohe File-Strings
Große CSV/LogsFiles.lines, BufferedReader, Batch-VerarbeitungreadAllLines bei unbekannter Größe
Sicherer ImportRoot-normalisierte Path-PolicyDateinamen direkt aus Requests übernehmen
Austausch mit anderen ProzessenTemp-Datei + atomic moveHalbfertige Datei direkt lesen
Code 51.1 – Sichere Import-Pipeline mit Root-Policy, SHA-256 und Quarantäne
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.security.*;
import java.time.Instant;
import java.util.HexFormat;
import java.util.function.Consumer;
import java.util.stream.Stream;

public final class SecureImportPipeline {
    private final Path root;
    private final Path inbox;
    private final Path archive;
    private final Path quarantine;

    public SecureImportPipeline(Path root) throws IOException {
        this.root = root.toAbsolutePath().normalize();
        this.inbox = create("inbox");
        this.archive = create("archive");
        this.quarantine = create("quarantine");
    }

    public void importOne(String externalName, Consumer<String> processor) throws IOException {
        Path source = resolveInside(inbox, externalName);
        Path work = source.resolveSibling(source.getFileName() + ".work");
        Files.move(source, work, StandardCopyOption.ATOMIC_MOVE);
        String checksum = "unknown";
        try {
            checksum = sha256(work);
            try (Stream<String> lines = Files.lines(work, StandardCharsets.UTF_8)) {
                lines.filter(line -> !line.isBlank())
                     .map(String::trim)
                     .forEach(processor);
            }
            Path target = archive.resolve(Instant.now().toEpochMilli() + "-" + checksum + ".txt");
            Files.move(work, target, StandardCopyOption.ATOMIC_MOVE);
        } catch (RuntimeException | IOException ex) {
            Path bad = quarantine.resolve(Instant.now().toEpochMilli() + "-" + checksum + ".bad");
            Files.move(work, bad, StandardCopyOption.REPLACE_EXISTING);
            throw ex;
        }
    }

    private Path create(String name) throws IOException {
        Path p = root.resolve(name).normalize();
        if (!p.startsWith(root)) throw new SecurityException("invalid root segment: " + name);
        return Files.createDirectories(p);
    }

    private static Path resolveInside(Path allowedRoot, String name) {
        Path resolved = allowedRoot.resolve(name).normalize().toAbsolutePath();
        if (!resolved.startsWith(allowedRoot.toAbsolutePath().normalize())) {
            throw new SecurityException("Path traversal rejected: " + name);
        }
        return resolved;
    }

    private static String sha256(Path file) throws IOException {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            try (InputStream in = Files.newInputStream(file)) {
                byte[] buf = new byte[8192];
                for (int n; (n = in.read(buf)) > 0; ) digest.update(buf, 0, n);
            }
            return HexFormat.of().formatHex(digest.digest());
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException(e);
        }
    }
}
Code 51.2 – Append-only Audit Log mit FileChannel und fsync
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.time.Instant;
import static java.nio.file.StandardOpenOption.*;

public final class DurableAuditLog implements AutoCloseable {
    private final FileChannel channel;

    public DurableAuditLog(Path file) throws IOException {
        Files.createDirectories(file.toAbsolutePath().getParent());
        this.channel = FileChannel.open(file, CREATE, WRITE, APPEND, DSYNC);
    }

    public synchronized void append(String actor, String action, String resource) throws IOException {
        String jsonLine = "{\"ts\":\"" + Instant.now() + "\","
                + "\"actor\":\"" + esc(actor) + "\","
                + "\"action\":\"" + esc(action) + "\","
                + "\"resource\":\"" + esc(resource) + "\"}\n";
        ByteBuffer buffer = StandardCharsets.UTF_8.encode(jsonLine);
        while (buffer.hasRemaining()) channel.write(buffer);
        channel.force(false); // Daten auf Storage-Barriere bringen
    }

    private static String esc(String s) {
        return s.replace("\\", "\\\\").replace("\"", "\\\"");
    }

    @Override public void close() throws IOException { channel.close(); }
}
Code 51.3 – Backpressure beim Dateiimport mit bounded Queue
import java.io.IOException;
import java.nio.file.*;
import java.util.concurrent.*;

public final class BoundedFileImporter implements AutoCloseable {
    private final BlockingQueue<Path> queue = new ArrayBlockingQueue<>(500);
    private final ExecutorService workers = Executors.newFixedThreadPool(4);
    private volatile boolean running = true;

    public BoundedFileImporter() {
        for (int i = 0; i < 4; i++) workers.submit(this::workerLoop);
    }

    public void submit(Path file) throws InterruptedException {
        if (!running) throw new RejectedExecutionException("Importer is stopping");
        queue.put(file); // blockiert bewusst: Producer bekommt Backpressure
    }

    private void workerLoop() {
        while (running || !queue.isEmpty()) {
            try {
                Path file = queue.poll(300, TimeUnit.MILLISECONDS);
                if (file != null) process(file);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            } catch (Exception e) {
                System.err.println("Import failed: " + e.getMessage());
            }
        }
    }

    private void process(Path file) throws IOException {
        long size = Files.size(file);
        if (size == 0 || size > 50_000_000L) throw new IOException("invalid size: " + size);
        // hier: parse, validate, persist, archive
    }

    @Override public void close() {
        running = false;
        workers.shutdown();
    }
}
Master-Regel: I/O-Code ist Infrastrukturcode. Er braucht Grenzen, Timeouts, Checksums, Metriken und eine klare Fehlerstrategie.
Warnsignal: Wenn eine Methode Pfade als rohe Strings annimmt und intern direkt liest oder schreibt, fehlt fast immer eine Sicherheitsgrenze.

Deep Dive: Datum, Zeit, Locale und Text – fachlich korrekt rechnen

Modul C · Runtime, I/O, Zeit & Concurrency · Alt-Referenz: K52

Zeit ist kein String und kein bloßes long. Professionelle Java-Systeme trennen Zeitpunkt, lokale Geschäftszeit, Zeitzone, Dauer, Periode, Formatierung und fachliche Kalenderregeln.

Zeitmodell: Instant, Zone, Locale und fachliche Regeln InstantglobalZoneIdlokalBusiness RuleCutoffLocaleFormatTextUser Output
InstantSpeichere technische Ereignisse meist als Zeitpunkt.
LocalDateNutze ihn für Geburtstage, Lieferdatum, Rechnungsdatum.
ZonedDateTimeNutze ihn, wenn eine lokale Uhrzeit in einer Zone fachlich zählt.
ClockInjiziere Zeit für testbaren Code.
ProblemGute LösungWarum
SommerzeitwechselZonedDateTime mit echter ZoneIdOffsets ändern sich historisch und zukünftig.
Business DeadlineLocalTime + Kalenderregel + ZoneDeadline ist fachlich, nicht technisch.
Mehrsprachige AusgabeDateTimeFormatter und NumberFormat mit LocaleKein manuelles String-Basteln.
TestsClock.fixed oder Clock.offsetKeine instabilen Tests durch now().
Code 52.1 – BusinessCalendar mit Feiertagen, Cutoff und testbarer Clock
import java.time.*;
import java.util.*;

public final class BusinessCalendar {
    private final ZoneId zone;
    private final LocalTime cutoff;
    private final Set<LocalDate> holidays;
    private final Clock clock;

    public BusinessCalendar(ZoneId zone, LocalTime cutoff, Set<LocalDate> holidays, Clock clock) {
        this.zone = Objects.requireNonNull(zone);
        this.cutoff = Objects.requireNonNull(cutoff);
        this.holidays = Set.copyOf(holidays);
        this.clock = Objects.requireNonNull(clock);
    }

    public LocalDate bookingDate() {
        ZonedDateTime now = ZonedDateTime.now(clock).withZoneSameInstant(zone);
        LocalDate candidate = now.toLocalTime().isAfter(cutoff)
                ? now.toLocalDate().plusDays(1)
                : now.toLocalDate();
        return nextBusinessDay(candidate);
    }

    public LocalDate nextBusinessDay(LocalDate date) {
        LocalDate d = date;
        while (isWeekend(d) || holidays.contains(d)) d = d.plusDays(1);
        return d;
    }

    public Duration untilCutoff() {
        ZonedDateTime now = ZonedDateTime.now(clock).withZoneSameInstant(zone);
        ZonedDateTime nextCutoff = now.toLocalDate().atTime(cutoff).atZone(zone);
        if (!nextCutoff.isAfter(now)) nextCutoff = nextCutoff.plusDays(1);
        return Duration.between(now, nextCutoff);
    }

    private static boolean isWeekend(LocalDate d) {
        return d.getDayOfWeek() == DayOfWeek.SATURDAY || d.getDayOfWeek() == DayOfWeek.SUNDAY;
    }
}
Code 52.2 – Locale-aware Invoice Renderer ohne externe Bibliothek
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.*;

public final class InvoiceRenderer {
    private static final Map<Locale, Map<String, String>> TEXT = Map.of(
        Locale.GERMANY, Map.of("title", "Rechnung", "due", "Fällig am", "total", "Summe"),
        Locale.US, Map.of("title", "Invoice", "due", "Due date", "total", "Total")
    );

    public String render(Invoice invoice, Locale locale, ZoneId zone) {
        Map<String, String> t = TEXT.getOrDefault(locale, TEXT.get(Locale.US));
        NumberFormat money = NumberFormat.getCurrencyInstance(locale);
        money.setCurrency(invoice.currency());
        DateTimeFormatter date = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale);
        LocalDate dueLocal = invoice.dueAt().atZone(zone).toLocalDate();
        return """
            %s #%s
            %s: %s
            %s: %s
            """.formatted(
                t.get("title"), invoice.number(),
                t.get("due"), date.format(dueLocal),
                t.get("total"), money.format(invoice.total())
            );
    }

    public record Invoice(String number, Instant dueAt, BigDecimal total, Currency currency) {}
}
Code 52.3 – Recurring Schedule mit Period vs Duration sauber modellieren
import java.time.*;
import java.util.ArrayList;
import java.util.List;

public final class RecurringSchedule {
    public enum Mode { CALENDAR_DAYS, EXACT_HOURS }

    public static List<ZonedDateTime> nextRuns(ZonedDateTime start, int count, Mode mode) {
        List<ZonedDateTime> result = new ArrayList<>();
        ZonedDateTime current = start;
        for (int i = 0; i < count; i++) {
            result.add(current);
            current = switch (mode) {
                case CALENDAR_DAYS -> current.plus(Period.ofDays(1)); // lokale Uhrzeit bleibt Ziel
                case EXACT_HOURS -> current.plus(Duration.ofHours(24)); // exakt 24h, DST kann Uhrzeit verschieben
            };
        }
        return result;
    }
}

Deep Dive: Nebenläufigkeit – kontrollierte Parallelität statt Thread-Chaos

Modul C · Runtime, I/O, Zeit & Concurrency · Alt-Referenz: K53

Virtual Threads lösen nicht automatisch Architekturprobleme. Sie machen blockierende Workloads günstiger, aber du brauchst weiterhin Backpressure, Timeouts, Cancellation, Bulkheads und thread-sichere Zustände.

Concurrency-Kontrollpunkte RequestInputBulkheadLimitTaskVirtual ThreadShared StateLock/AtomicResultTimeout
ThroughputWie viele Aufgaben pro Zeitfenster?
ConcurrencyWie viele Aufgaben gleichzeitig?
ParallelismWie viele CPU-Kerne arbeiten wirklich?
ContentionWo konkurrieren Threads um denselben Zustand?
SituationPassendes WerkzeugMeisterhinweis
Viele blockierende I/O-AufrufeVirtual Threads + Semaphore BulkheadVirtual Threads nicht unlimitiert gegen fremde Systeme feuern.
CPU-lastige ArbeitFixed Pool nahe CPU-KernenMehr Threads machen CPU-Code nicht schneller.
Shared Counter/ConfigAtomic/Immutable SnapshotSperren klein halten.
Producer schneller als ConsumerBounded QueueUnbounded Queues sind versteckte Memory Leaks.
Code 53.1 – Virtual-Thread-Service mit Bulkhead, Deadline und sauberer Cancellation
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Supplier;

public final class VirtualThreadBulkhead implements AutoCloseable {
    private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
    private final Semaphore permits;

    public VirtualThreadBulkhead(int maxConcurrent) {
        this.permits = new Semaphore(maxConcurrent);
    }

    public <T> T call(Duration timeout, Supplier<T> task) throws Exception {
        if (!permits.tryAcquire(timeout)) throw new TimeoutException("bulkhead full");
        Future<T> future = executor.submit(() -> {
            try { return task.get(); }
            finally { permits.release(); }
        });
        try {
            return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            future.cancel(true); // Interrupt an den Virtual Thread
            throw e;
        }
    }

    public <T> List<T> callAll(Collection<Supplier<T>> tasks, Duration timeout) throws Exception {
        List<Future<T>> futures = new ArrayList<>();
        for (Supplier<T> task : tasks) {
            permits.acquire();
            futures.add(executor.submit(() -> {
                try { return task.get(); }
                finally { permits.release(); }
            }));
        }
        List<T> out = new ArrayList<>();
        long deadline = System.nanoTime() + timeout.toNanos();
        for (Future<T> f : futures) {
            long left = deadline - System.nanoTime();
            if (left <= 0) throw new TimeoutException("batch timeout");
            out.add(f.get(left, TimeUnit.NANOSECONDS));
        }
        return out;
    }

    @Override public void close() { executor.close(); }
}
Code 53.2 – Producer/Consumer mit Poison Pill und Fehlerkanal
import java.util.*;
import java.util.concurrent.*;

public final class ImportQueue<T> implements AutoCloseable {
    private final BlockingQueue<Item<T>> queue;
    private final ExecutorService workers;
    private final List<Throwable> errors = Collections.synchronizedList(new ArrayList<>());
    private final Item<T> stop = new Item<>(null, true);

    public ImportQueue(int capacity, int workerCount, CheckedConsumer<T> consumer) {
        this.queue = new ArrayBlockingQueue<>(capacity);
        this.workers = Executors.newFixedThreadPool(workerCount);
        for (int i = 0; i < workerCount; i++) {
            workers.submit(() -> loop(consumer));
        }
    }

    public void submit(T item) throws InterruptedException {
        queue.put(new Item<>(Objects.requireNonNull(item), false));
    }

    private void loop(CheckedConsumer<T> consumer) {
        try {
            while (true) {
                Item<T> item = queue.take();
                if (item.stop()) return;
                try { consumer.accept(item.value()); }
                catch (Throwable t) { errors.add(t); }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public List<Throwable> errors() { return List.copyOf(errors); }

    @Override public void close() throws InterruptedException {
        for (int i = 0; i < 4; i++) queue.put(stop);
        workers.shutdown();
    }

    private record Item<T>(T value, boolean stop) {}
    public interface CheckedConsumer<T> { void accept(T value) throws Exception; }
}
Code 53.3 – Immutable Config Snapshot mit AtomicReference
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;

public final class RuntimeConfig {
    private final AtomicReference<Snapshot> current = new AtomicReference<>(new Snapshot(50, Duration.ofSeconds(2), true));

    public Snapshot get() { return current.get(); }

    public void update(Snapshot next) {
        current.set(Objects.requireNonNull(next)); // atomarer Austausch, keine halbfertige Config sichtbar
    }

    public boolean tryUpdate(int expectedVersion, Snapshot next) {
        Snapshot old = current.get();
        return old.version() == expectedVersion && current.compareAndSet(old, next);
    }

    public record Snapshot(int maxConcurrent, Duration timeout, boolean featureEnabled, int version) {
        public Snapshot(int maxConcurrent, Duration timeout, boolean featureEnabled) {
            this(maxConcurrent, timeout, featureEnabled, 1);
        }
        public Snapshot {
            if (maxConcurrent < 1 || maxConcurrent > 10_000) throw new IllegalArgumentException("maxConcurrent");
            if (timeout.isNegative() || timeout.isZero()) throw new IllegalArgumentException("timeout");
        }
    }
}

Deep Dive: CompletableFuture – Orchestrierung, Fallbacks und Hedging

Modul C · Runtime, I/O, Zeit & Concurrency · Alt-Referenz: K54

CompletableFuture ist kein Ersatz für sauberes Workflow-Design. Du modellierst Abhängigkeiten, Timeouts, Fehlerkanäle und Fallbacks explizit. Wichtig ist: niemals Exceptions verschlucken und niemals unbegrenzt auf dem Common Pool blockieren.

Async-Workflow: abhängig, parallel, fehlertolerant InputRequestParallelProfile + RiskTimeoutBudgetFallbackDegradedComposeDecision
MusterNutzenAchtung
thenComposeSequenzielle Abhängigkeit flach haltenNicht mit thenApply verschachteln.
allOfParallel wartenErgebnisse müssen danach typisiert eingesammelt werden.
orTimeoutLatenzbudget erzwingenTimeout ist kein Abbruch fremder I/O ohne Cancellation.
exceptionally/handleFallbacks definierenFehler nicht zu Erfolg umlabeln, wenn Business scheitert.
Code 54.1 – Async Order Decision mit typed Result, Timeout und Fallback
import java.time.Duration;
import java.util.concurrent.*;

public final class AsyncDecisionService {
    private final Executor executor;
    private final CustomerClient customers;
    private final RiskClient risk;
    private final InventoryClient inventory;

    public AsyncDecisionService(Executor executor, CustomerClient customers, RiskClient risk, InventoryClient inventory) {
        this.executor = executor;
        this.customers = customers;
        this.risk = risk;
        this.inventory = inventory;
    }

    public CompletableFuture<Result<Decision>> decide(OrderRequest request) {
        CompletableFuture<Customer> customerF = supply(() -> customers.load(request.customerId()), "customer");
        CompletableFuture<RiskScore> riskF = customerF.thenCompose(c -> supply(() -> risk.score(c), "risk"));
        CompletableFuture<Boolean> stockF = supply(() -> inventory.available(request.sku(), request.quantity()), "inventory")
                .completeOnTimeout(false, 500, TimeUnit.MILLISECONDS);

        return customerF.thenCombine(riskF, Context::new)
                .thenCombine(stockF, (ctx, stock) -> {
                    if (!stock) return Result.ok(new Decision(false, "out_of_stock"));
                    if (ctx.risk().value() > 70) return Result.ok(new Decision(false, "risk_rejected"));
                    return Result.ok(new Decision(true, "accepted"));
                })
                .orTimeout(1200, TimeUnit.MILLISECONDS)
                .exceptionally(ex -> Result.fail("decision_failed", ex));
    }

    private <T> CompletableFuture<T> supply(Callable<T> callable, String stage) {
        return CompletableFuture.supplyAsync(() -> {
            try { return callable.call(); }
            catch (Exception e) { throw new CompletionException(stage + " failed", e); }
        }, executor);
    }

    public record OrderRequest(String customerId, String sku, int quantity) {}
    public record Customer(String id) {}
    public record RiskScore(int value) {}
    public record Context(Customer customer, RiskScore risk) {}
    public record Decision(boolean accepted, String reason) {}

    public sealed interface Result<T> permits Result.Ok, Result.Fail {
        record Ok<T>(T value) implements Result<T> {}
        record Fail<T>(String code, Throwable cause) implements Result<T> {}
        static <T> Result<T> ok(T value) { return new Ok<>(value); }
        static <T> Result<T> fail(String code, Throwable cause) { return new Fail<>(code, cause); }
    }

    interface CustomerClient { Customer load(String id) throws Exception; }
    interface RiskClient { RiskScore score(Customer customer) throws Exception; }
    interface InventoryClient { boolean available(String sku, int quantity) throws Exception; }
}
Code 54.2 – Hedged Request: langsamste Replik wird abgebrochen
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.*;

public final class HedgedHttpClient {
    private final HttpClient client = HttpClient.newHttpClient();
    private final ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();

    public CompletableFuture<String> getFastest(List<URI> replicas, Duration hedgeDelay) {
        CompletableFuture<String> winner = new CompletableFuture<>();
        CopyOnWriteArrayList<CompletableFuture<?>> running = new CopyOnWriteArrayList<>();

        for (int i = 0; i < replicas.size(); i++) {
            URI uri = replicas.get(i);
            long delayMs = hedgeDelay.toMillis() * i;
            timer.schedule(() -> {
                if (winner.isDone()) return;
                CompletableFuture<String> f = send(uri);
                running.add(f);
                f.whenComplete((body, ex) -> {
                    if (ex == null) winner.complete(body);
                    else if (running.stream().allMatch(CompletableFuture::isDone)) winner.completeExceptionally(ex);
                });
            }, delayMs, TimeUnit.MILLISECONDS);
        }

        winner.whenComplete((v, ex) -> running.forEach(f -> f.cancel(true)));
        return winner.orTimeout(2, TimeUnit.SECONDS);
    }

    private CompletableFuture<String> send(URI uri) {
        HttpRequest req = HttpRequest.newBuilder(uri).timeout(Duration.ofSeconds(1)).GET().build();
        return client.sendAsync(req, HttpResponse.BodyHandlers.ofString())
                .thenApply(HttpResponse::body);
    }
}
Code 54.3 – Async Batch: Ergebnisse sammeln, Fehler nicht verlieren
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Function;

public final class AsyncBatch {
    public static <I, O> CompletableFuture<BatchResult<I, O>> run(
            List<I> input, Function<I, CompletableFuture<O>> fn) {
        List<CompletableFuture<ItemResult<I, O>>> futures = input.stream()
                .map(item -> fn.apply(item)
                        .thenApply(value -> ItemResult.ok(item, value))
                        .exceptionally(ex -> ItemResult.fail(item, ex)))
                .toList();

        return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
                .thenApply(ignored -> new BatchResult<>(futures.stream().map(CompletableFuture::join).toList()));
    }

    public record BatchResult<I, O>(List<ItemResult<I, O>> items) {
        public boolean hasFailures() { return items.stream().anyMatch(i -> i instanceof ItemResult.Fail<I, O>); }
    }

    public sealed interface ItemResult<I, O> permits ItemResult.Ok, ItemResult.Fail {
        I input();
        record Ok<I, O>(I input, O value) implements ItemResult<I, O> {}
        record Fail<I, O>(I input, Throwable error) implements ItemResult<I, O> {}
        static <I, O> ItemResult<I, O> ok(I input, O value) { return new Ok<>(input, value); }
        static <I, O> ItemResult<I, O> fail(I input, Throwable error) { return new Fail<>(input, error); }
    }
}

Deep Dive: Speicher, GC und Leak-Diagnose – messen, begrenzen, freigeben

Modul C · Runtime, I/O, Zeit & Concurrency · Alt-Referenz: K55

Java-GC befreit dich von manuellem free(), aber nicht von Lebensdauer-Design. Fast alle Java-Leaks entstehen, weil ein Objekt noch erreichbar ist: durch statische Maps, Listener, Caches, Queues, ThreadLocals oder lange Sessions.

Heap-Lebensdauer: Allokation bis Freigabe AllocateObjektReferenceGraphScopeLifetimeGC RootReachable?Collectfrei
AllokationsrateWie viele Objekte entstehen pro Sekunde?
Retained SizeWie viel Heap hält ein Objektgraph fest?
GC PauseWie lange stoppt oder verlangsamt GC dein System?
Bounded ResourcesCaches, Queues und Pools brauchen harte Grenzen.
Code 55.1 – Bounded TTL+LRU Cache mit expliziter Lebensdauer
import java.time.*;
import java.util.*;
import java.util.function.Function;

public final class TtlLruCache<K, V> {
    private final int maxEntries;
    private final Duration ttl;
    private final Clock clock;
    private final LinkedHashMap<K, Entry<V>> map;

    public TtlLruCache(int maxEntries, Duration ttl, Clock clock) {
        if (maxEntries < 1) throw new IllegalArgumentException("maxEntries");
        this.maxEntries = maxEntries;
        this.ttl = ttl;
        this.clock = clock;
        this.map = new LinkedHashMap<>(16, 0.75f, true) {
            @Override protected boolean removeEldestEntry(Map.Entry<K, Entry<V>> eldest) {
                return size() > TtlLruCache.this.maxEntries;
            }
        };
    }

    public synchronized V get(K key, Function<K, V> loader) {
        Instant now = clock.instant();
        Entry<V> entry = map.get(key);
        if (entry != null && entry.expiresAt().isAfter(now)) return entry.value();
        V loaded = Objects.requireNonNull(loader.apply(key));
        map.put(key, new Entry<>(loaded, now.plus(ttl)));
        return loaded;
    }

    public synchronized void purgeExpired() {
        Instant now = clock.instant();
        map.entrySet().removeIf(e -> !e.getValue().expiresAt().isAfter(now));
    }

    public synchronized int size() { return map.size(); }
    private record Entry<V>(V value, Instant expiresAt) {}
}
Code 55.2 – Leak-sicherer Listener Registry mit AutoCloseable Subscription
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;

public final class ListenerRegistry<E> {
    private final CopyOnWriteArrayList<WeakReference<Consumer<E>>> listeners = new CopyOnWriteArrayList<>();

    public AutoCloseable subscribe(Consumer<E> listener) {
        WeakReference<Consumer<E>> ref = new WeakReference<>(Objects.requireNonNull(listener));
        listeners.add(ref);
        return () -> listeners.remove(ref);
    }

    public void publish(E event) {
        for (WeakReference<Consumer<E>> ref : listeners) {
            Consumer<E> listener = ref.get();
            if (listener == null) listeners.remove(ref);
            else listener.accept(event);
        }
    }

    public int approximateSubscriberCount() {
        listeners.removeIf(ref -> ref.get() == null);
        return listeners.size();
    }
}
Code 55.3 – Lightweight Memory Probe für lokale Diagnose
import java.lang.management.*;
import java.time.Instant;

public final class MemoryProbe {
    private final MemoryMXBean memory = ManagementFactory.getMemoryMXBean();

    public Snapshot snapshot(String label) {
        MemoryUsage heap = memory.getHeapMemoryUsage();
        MemoryUsage nonHeap = memory.getNonHeapMemoryUsage();
        return new Snapshot(label, Instant.now(), heap.getUsed(), heap.getCommitted(), heap.getMax(), nonHeap.getUsed());
    }

    public void printDelta(Snapshot before, Snapshot after) {
        System.out.printf("%s -> %s heap delta: %,d bytes%n",
                before.label(), after.label(), after.heapUsed() - before.heapUsed());
    }

    public record Snapshot(String label, Instant ts, long heapUsed, long heapCommitted, long heapMax, long nonHeapUsed) {}
}
Master-Regel: Jede Collection, Queue und jeder Cache in langlebigen Services braucht eine Grenze oder eine klare Eviction-Regel.
Warnsignal: static Map, ThreadLocal und Listener ohne Deregistrierung sind typische Leak-Startpunkte.

Deep Dive: Module, Packages und Architekturgrenzen – Codebase skalierbar halten

Modul C · Runtime, I/O, Zeit & Concurrency · Alt-Referenz: K56

Architekturgrenzen sind nicht nur Diagramme. In Java kannst du sie mit Packages, Modulen, Interfaces, ServiceLoader und Build-Regeln erzwingen. Ein Master erkennt Abhängigkeiten, bevor sie zyklisch, implizit oder schwer testbar werden.

Architekturgrenzen: API nach außen, Details nach innen domainkeine InfraapplicationUse CasesportsInterfacesadaptersI/O, DB, HTTPbootstrapWiring
EbeneDarf kennenDarf nicht kennen
DomainValue Objects, Domain ServicesHTTP, SQL, Frameworks, Clock.now direkt
ApplicationPorts, Transactions, Use CasesKonkrete Adapterklassen
AdapterJDBC, Files, HTTP, JSONAndere Adapter-Interna
BootstrapKonkretes WiringFachlogik
Code 56.1 – JPMS Service Provider: Plugin-API mit module-info
// module com.seb4u.demo.payments.api
module com.seb4u.demo.payments.api {
    exports com.seb4u.demo.payments.api;
}

// package com.seb4u.demo.payments.api
package com.seb4u.demo.payments.api;

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

public interface PaymentProvider {
    String name();
    PaymentResult authorize(PaymentRequest request);
}

public record PaymentRequest(String orderId, BigDecimal amount, Currency currency) {}
public record PaymentResult(boolean accepted, String providerReference, String reason) {}

// module com.seb4u.demo.payments.fake
module com.seb4u.demo.payments.fake {
    requires com.seb4u.demo.payments.api;
    provides com.seb4u.demo.payments.api.PaymentProvider
        with com.seb4u.demo.payments.fake.FakePaymentProvider;
}
Code 56.2 – ServiceLoader Registry mit deterministischer Auswahl
package com.seb4u.demo.checkout.bootstrap;

import com.seb4u.demo.payments.api.PaymentProvider;
import java.util.*;

public final class PaymentProviderRegistry {
    private final Map<String, PaymentProvider> providers;

    public PaymentProviderRegistry(ServiceLoader<PaymentProvider> loader) {
        Map<String, PaymentProvider> tmp = new TreeMap<>();
        for (PaymentProvider provider : loader) {
            PaymentProvider previous = tmp.putIfAbsent(provider.name(), provider);
            if (previous != null) throw new IllegalStateException("duplicate provider: " + provider.name());
        }
        this.providers = Map.copyOf(tmp);
    }

    public PaymentProvider require(String name) {
        PaymentProvider provider = providers.get(name);
        if (provider == null) throw new NoSuchElementException("payment provider not found: " + name);
        return provider;
    }

    public Set<String> names() { return providers.keySet(); }
}
Code 56.3 – Architekturgrenze durch Port statt Adapter-Abhängigkeit
package com.seb4u.demo.application;

import com.seb4u.demo.domain.*;
import java.util.Objects;

public final class PlaceOrderUseCase {
    private final OrderRepository orders;
    private final PaymentPort payments;
    private final DomainEventPublisher events;

    public PlaceOrderUseCase(OrderRepository orders, PaymentPort payments, DomainEventPublisher events) {
        this.orders = Objects.requireNonNull(orders);
        this.payments = Objects.requireNonNull(payments);
        this.events = Objects.requireNonNull(events);
    }

    public OrderId place(PlaceOrder command) {
        Order order = Order.draft(command.customerId());
        command.lines().forEach(line -> order.add(line.sku(), line.quantity()));
        PaymentReceipt receipt = payments.authorize(order.id(), order.total());
        order.markPaid(receipt.reference());
        orders.save(order);
        events.publishAll(order.pullEvents());
        return order.id();
    }

    public interface OrderRepository { void save(Order order); }
    public interface PaymentPort { PaymentReceipt authorize(OrderId id, Money total); }
    public interface DomainEventPublisher { void publishAll(java.util.List<DomainEvent> events); }
}

Deep Dive: Build, Dependencies und reproduzierbare Projekte

Modul C · Runtime, I/O, Zeit & Concurrency · Alt-Referenz: K57

Ein professioneller Build ist wiederholbar, prüfbar und teamfähig. Er definiert Toolchain, Versionen, Teststufen, Artefakte, Checks und Betriebsinformationen. Das Ziel ist: Ein frischer Rechner kann das Projekt deterministisch bauen.

Build-Pipeline: lokal gleich CI gleich Release ToolchainJDKResolveDepsTestQuality GatesPackageArtifactVerifySBOM/Checksum
Version PinningKeine schwebenden Versionen in produktiven Builds.
WrapperMaven/Gradle Wrapper hält Team und CI konsistent.
Build InfoArtefakte sollen Version, Commit und Buildzeit kennen.
Dependency HygieneAbhängigkeiten regelmäßig prüfen, aber kontrolliert aktualisieren.
Code 57.1 – Maven POM mit Toolchain, Enforcer und reproduzierbarer Jar-Konfiguration
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>dev.master</groupId>
  <artifactId>commerce-core</artifactId>
  <version>1.0.0</version>
  <properties>
    <maven.compiler.release>25</maven.compiler.release>
    <project.build.outputTimestamp>2026-01-01T00:00:00Z</project.build.outputTimestamp>
  </properties>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-enforcer-plugin</artifactId>
        <version>3.6.1</version>
        <executions><execution><goals><goal>enforce</goal></goals></execution></executions>
        <configuration>
          <rules>
            <requireJavaVersion><version>[25,26)</version></requireJavaVersion>
            <dependencyConvergence />
          </rules>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
Code 57.2 – Gradle Kotlin DSL mit Java Toolchain und Test-Gates
plugins {
    java
    application
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(25))
    }
}

tasks.withType<Test>().configureEach {
    useJUnitPlatform()
    testLogging {
        events("failed", "skipped")
        exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
    }
}

tasks.jar {
    manifest {
        attributes(
            "Implementation-Title" to project.name,
            "Implementation-Version" to project.version,
            "Built-By" to "reproducible-build"
        )
    }
}
Code 57.3 – BuildInfo im Artefakt lesen und im Health Endpoint ausgeben
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.util.Properties;

public final class BuildInfo {
    private final Properties props = new Properties();

    public static BuildInfo load() {
        BuildInfo info = new BuildInfo();
        try (InputStream in = BuildInfo.class.getResourceAsStream("/build-info.properties")) {
            if (in != null) info.props.load(in);
        } catch (IOException e) {
            throw new IllegalStateException("Cannot load build-info.properties", e);
        }
        return info;
    }

    public String version() { return props.getProperty("version", "dev"); }
    public String commit() { return props.getProperty("commit", "unknown"); }
    public Instant buildTime() { return Instant.parse(props.getProperty("time", "1970-01-01T00:00:00Z")); }

    public String asJson() {
        return "{\"version\":\"" + version() + "\",\"commit\":\"" + commit() + "\",\"time\":\"" + buildTime() + "\"}";
    }
}
Code 57.4 – Dependency Fingerprint für einfache Supply-Chain-Kontrolle
import java.io.IOException;
import java.nio.file.*;
import java.security.*;
import java.util.*;
import java.util.HexFormat;
import java.util.stream.Stream;

public final class DependencyFingerprint {
    public static Map<String, String> fingerprint(Path libDir) throws IOException {
        Map<String, String> result = new TreeMap<>();
        try (Stream<Path> files = Files.list(libDir)) {
            for (Path jar : files.filter(p -> p.toString().endsWith(".jar")).toList()) {
                result.put(jar.getFileName().toString(), sha256(jar));
            }
        }
        return result;
    }

    private static String sha256(Path file) throws IOException {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            md.update(Files.readAllBytes(file));
            return HexFormat.of().formatHex(md.digest());
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException(e);
        }
    }
}
Master-Regel: Build-Logik ist Produktionscode. Sie entscheidet, was wirklich ausgeliefert wird.
Warnsignal: „Bei mir funktioniert es“ heißt oft: Toolchain, Environment oder Dependency-Versionen sind nicht reproduzierbar.
Modul D

🏗️ Professional Backend, Architektur & Code-Labs

Testing, HTTP, JDBC, Security, Patterns, DDD, Performance, Observability, Refactoring und komplexe Code-Labs.

25Themen in diesem Modul
82Codeblöcke
33SVG-Grafiken
🧪

Testing: Von Unit bis Contract

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K19

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Testpyramide Testpyramide Viele Unit Tests: schnell, isoliert Integrationstests: DB, Files, Netzwerkgrenzen Contract Tests: Schnittstellenverträge End-to-End Tests: wenige, teuer, kritisch Je höher der Test, desto teurer und fragiler ist er meist.

Tests sind Designfeedback. Gut testbarer Code hat klare Abhängigkeiten, kleine Einheiten, wenige globale Zustände und explizite Schnittstellen. Unit Tests prüfen Fachlogik ohne Infrastruktur. Integrationstests prüfen Zusammenspiel mit Datenbank, Dateisystem oder externen Komponenten. End-to-End Tests prüfen kritische Nutzerflüsse.

Teste Verhalten, nicht Implementierungsdetails. Ein Test, der bei jedem Refactoring bricht, obwohl das Verhalten gleich bleibt, ist zu eng gekoppelt. Verwende Testdaten-Builder, klare Namen und Arrange-Act-Assert-Struktur.

Masterblick: Ein guter Test erklärt die Fachregel besser als ein Kommentar.

Beispielcode

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class MoneyTest {
    @Test void addsOnlySameCurrency() {
        var a = Money.eur("10.00");
        var b = Money.eur("2.50");
        assertEquals(Money.eur("12.50"), a.plus(b));
    }
}

Übungen

  • Schreibe Tests für Value Objects.
  • Baue einen Testdaten-Builder.
  • Entferne unnötige Mocks aus einem Test.

Mastery-Check

  • Du kennst Testpyramide und Trade-offs.
  • Du testest Verhalten statt Implementierung.
  • Du nutzt Tests als Refactoring-Schutz.
🌐

HTTP, REST, JSON und Systemgrenzen

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K20

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

HTTP-Grenze HTTP-Grenze Request DTO Validation Use Case Result Response An Systemgrenzen werden Formate validiert und Fehler übersetzt.

HTTP-APIs sind Verträge zwischen Systemen. Trenne Transportmodelle von Domänenmodellen. Ein JSON-DTO darf optional, unvollständig oder versioniert sein; ein Domänenobjekt sollte gültig und invariantensicher sein.

Behandle Statuscodes bewusst: 200 für Erfolg, 201 für Erstellung, 400 für ungültige Syntax, 401/403 für Authentifizierung/Autorisierung, 404 für nicht gefunden, 409 für Konflikte, 422 für fachlich ungültige Daten, 500 für unerwartete Serverfehler.

Timeouts, Retries und Idempotenz sind essenziell. Ein Retry ohne Idempotenz kann doppelte Zahlungen, doppelte Bestellungen oder inkonsistente Zustände verursachen.

Beispielcode

var client = java.net.http.HttpClient.newBuilder()
    .connectTimeout(java.time.Duration.ofSeconds(2))
    .build();

var request = java.net.http.HttpRequest.newBuilder()
    .uri(java.net.URI.create("https://example.invalid/api/customers/42"))
    .timeout(java.time.Duration.ofSeconds(3))
    .GET()
    .build();

// client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString());

Übungen

  • Entwerfe DTOs getrennt von Domain Records.
  • Lege Fehlerantworten als einheitliches Format fest.
  • Beschreibe Idempotenz für POST vs PUT.

Mastery-Check

  • Du trennst API- und Domainmodell.
  • Du setzt Timeouts konsequent.
  • Du kennst grundlegende HTTP-Statuscodes.
🗄️

Datenbanken, JDBC und Transaktionen

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K21

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Transaktion Transaktion Service Use Case Commit/Rollback Connection begin execute commit DB lock row write release Transaktionen definieren, welche Änderungen gemeinsam gültig werden.

Datenbanken sind nicht nur Speicher, sondern Konsistenzsysteme. JDBC ist die Standardbasis für SQL-Zugriff in Java. Auch wenn du später JPA oder andere Frameworks nutzt, solltest du Connection, PreparedStatement, ResultSet, Transaktionen und Isolation verstehen.

Nutze Prepared Statements, um SQL Injection zu verhindern. Verwalte Transaktionen bewusst: Was gehört zusammen? Wann committen? Was passiert bei Fehlern? Welche Isolation brauchst du gegen Lost Updates, Dirty Reads oder Phantom Reads?

Masterblick: Viele Backend-Bugs sind keine Java-Bugs, sondern Transaktions- und Konkurrenzprobleme.

Beispielcode

try (var con = dataSource.getConnection()) {
    con.setAutoCommit(false);
    try (var ps = con.prepareStatement(
        "update account set balance = balance - ? where id = ? and balance >= ?")) {
        ps.setBigDecimal(1, amount);
        ps.setString(2, accountId);
        ps.setBigDecimal(3, amount);
        int updated = ps.executeUpdate();
        if (updated != 1) throw new IllegalStateException("insufficient_funds");
        con.commit();
    } catch (Exception e) {
        con.rollback();
        throw e;
    }
}

Übungen

  • Schreibe ein DAO mit PreparedStatement.
  • Simuliere zwei parallele Updates.
  • Dokumentiere die gewünschte Isolation für einen Use Case.

Mastery-Check

  • Du nutzt Prepared Statements.
  • Du verstehst Commit und Rollback.
  • Du erkennst Lost-Update-Risiken.
🛡️

Sicherheit: Eingaben, Secrets und Rechte

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K22

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Security Defense in Depth Security Defense in Depth Validierung und Canonicalization Authentifizierung Autorisierung Secret Management Logging ohne sensible Daten Patching und Dependency Hygiene Sicherheit ist Schichtarbeit, nicht eine einzelne Library.

Sichere Java-Entwicklung beginnt bei Eingaben. Alles, was von außen kommt, ist potenziell falsch, zu groß, bösartig oder widersprüchlich. Validiere Syntax, Länge, Wertebereich und Kontext. Escaping hängt vom Zielkontext ab: HTML, SQL, Shell, JSON und Regex sind verschieden.

Secrets gehören nicht in Git, Logs oder Fehlermeldungen. Nutze Umgebungsvariablen, Secret Stores oder Plattformmechanismen. Logge Korrelationen, aber keine Passwörter, Tokens, vollständigen Karten- oder Ausweisnummern.

Autorisierung ist mehr als Login. Prüfe, ob ein authentifizierter Benutzer diese konkrete Ressource mit dieser Operation verwenden darf.

Beispielcode

record Email(String value) {
    Email {
        if (value == null || value.length() > 254 || !value.contains("@")) {
            throw new IllegalArgumentException("invalid_email");
        }
        value = value.strip().toLowerCase(java.util.Locale.ROOT);
    }
}

boolean canViewInvoice(User user, Invoice invoice) {
    return user.roles().contains("ADMIN") || invoice.customerId().equals(user.customerId());
}

Übungen

  • Suche nach Secrets in deinem Repository.
  • Schreibe eine Autorisierungsprüfung pro Ressource.
  • Erstelle eine Liste sensibler Log-Felder.

Mastery-Check

  • Du validierst Eingaben kontextabhängig.
  • Du loggst keine Secrets.
  • Du trennst Authentifizierung und Autorisierung.
🎛️

Design Patterns ohne Dogma

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K23

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Pattern-Matrix Pattern-Matrix Objekterzeugung Algorithmuswahl Fremdsystem Ereignisse Factory - - - Strategy - ~ - Adapter - - ~ Observer - ~ - Patterns sind Vokabular für wiederkehrende Entwurfsprobleme.

Design Patterns sind keine Checkliste, sondern Sprache. Factory kapselt Erzeugung. Strategy macht austauschbare Algorithmen explizit. Adapter übersetzt zwischen inkompatiblen Schnittstellen. Observer/Eventing entkoppelt Sender und Empfänger. Decorator erweitert Verhalten ohne Unterklassenexplosion.

Vermeide Pattern-Theater: Eine einfache Methode ist oft besser als fünf Klassen mit abstrakten Namen. Nutze Patterns, wenn sie Abhängigkeiten reduzieren, Tests erleichtern oder Varianten sauber modellieren.

Masterregel: Benenne Patterns im Code nur, wenn das den Zweck klärt. `PaymentStrategy` ist sinnvoll; `AbstractPaymentStrategyFactoryManager` ist ein Warnsignal.

Beispielcode

interface TaxStrategy { Money taxFor(Money net); }
record GermanVat() implements TaxStrategy {
    public Money taxFor(Money net) { return net.multiply("0.19"); }
}
record NoTax() implements TaxStrategy {
    public Money taxFor(Money net) { return Money.zero(net.currency()); }
}

Übungen

  • Implementiere Strategy für Rabattregeln.
  • Schreibe einen Adapter für eine fremde API.
  • Entferne ein unnötiges Pattern aus Beispielcode.

Mastery-Check

  • Du nutzt Patterns problemorientiert.
  • Du erkennst Overengineering.
  • Du kannst mindestens fünf Patterns praktisch erklären.
🏛️

Domain-Driven Design und Hexagonale Architektur

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K24

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Hexagonale Architektur Hexagonale Architektur Domain: Entities, Value Objects, Domain Services Application: Use Cases, Ports, Transactions Adapters: REST, DB, Messaging, CLI Frameworks: austauschbare Details Die Domäne bleibt innen; technische Details bleiben außen.

Domain-Driven Design hilft bei komplexer Fachlichkeit. Die Domäne ist der Bereich, in dem Regeln, Sprache und Entscheidungen leben. Entities haben Identität. Value Objects haben Wertsemantik. Aggregates schützen Konsistenzgrenzen. Repositories abstrahieren Persistenz. Domain Events beschreiben fachlich relevante Ereignisse.

Hexagonale Architektur trennt den fachlichen Kern von technischen Adaptern. Use Cases definieren Ports. Adapter implementieren diese Ports für Datenbank, HTTP, Messaging oder externe Systeme.

Masterblick: Gute Architektur lässt dich Fachlogik testen, ohne Server, Datenbank oder Framework starten zu müssen.

Beispielcode

interface AccountRepository {
    Optional<Account> find(AccountId id);
    void save(Account account);
}

final class TransferMoneyUseCase {
    private final AccountRepository accounts;
    TransferMoneyUseCase(AccountRepository accounts) { this.accounts = accounts; }
    void transfer(AccountId from, AccountId to, Money amount) {
        var source = accounts.find(from).orElseThrow();
        var target = accounts.find(to).orElseThrow();
        source.withdraw(amount);
        target.deposit(amount);
        accounts.save(source);
        accounts.save(target);
    }
}

Übungen

  • Definiere Entities, Value Objects und Aggregates für einen Shop.
  • Ziehe einen Port aus einem direkten Datenbankzugriff heraus.
  • Teste einen Use Case ohne Infrastruktur.

Mastery-Check

  • Du erkennst Domänenlogik.
  • Du verstehst Ports und Adapter.
  • Du schützt Aggregate-Invarianten.
🚀

Performance: Messen, nicht raten

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K25

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Performance-Zyklus Performance-Zyklus Hypothese bilden Messen Engpass finden Ändern Vergleichen Optimierung ohne Messung ist Glücksspiel.

Performance beginnt mit Zielen: Latenz, Durchsatz, Speicher, CPU, Startzeit, Kosten. Ohne Ziel ist „schnell“ bedeutungslos. Miss realistische Szenarien, nicht nur Mikrobenchmarks. Prüfe Datenbank, Netzwerk, Serialisierung, Garbage Collection, Lock Contention und Algorithmik.

Häufige Java-Performanceprobleme: unnötige Objektallokation, falsche Collections, N+1 Queries, zu große JSON-Payloads, blockierte Thread Pools, fehlende Timeouts, übermäßige Logs, ineffiziente Regex und schlechte Caches.

Masterregel: Erst Algorithmus und Systemgrenzen, dann Mikrooptimierung.

Beispielcode

long start = System.nanoTime();
try {
    runUseCase();
} finally {
    long ms = java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
    System.out.println("duration_ms=" + ms);
}

Übungen

  • Messe eine Methode mit realistischen Daten.
  • Finde eine O(n²)-Stelle und verbessere sie.
  • Vergleiche Speicherverbrauch zweier Datenstrukturen.

Mastery-Check

  • Du definierst Performanceziele.
  • Du misst vor Optimierung.
  • Du erkennst häufige Engpässe.
📡

Observability, Logging und Diagnose

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K26

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Observability-Pipeline Observability-Pipeline Log Event Metric Trace Dashboard Alert Incident Review Produktionsreife bedeutet: Fehler sind auffindbar, erklärbar und nachverfolgbar.

Observability beantwortet, was ein System gerade tut und warum. Logs beschreiben Ereignisse. Metriken zeigen Trends und Schwellen. Traces verfolgen Requests über Servicegrenzen. Gute Diagnose benötigt Korrelation: Request-ID, User-Kontext ohne sensible Daten, Operation und Fehlerklasse.

Logs sollten strukturiert, knapp und nützlich sein. `System.out.println` reicht für Lernbeispiele, aber nicht für Produktion. In echten Systemen nutzt man Logging-Frameworks, Metrik-Exporter und Tracing.

Masterblick: Ein Fehler, der nur lokal reproduzierbar ist, kostet weniger. Ein Fehler, der nur in Produktion sichtbar ist und keine Spuren hinterlässt, kostet sehr viel.

Beispielcode

record AuditEvent(String requestId, String action, String result, java.time.Instant at) {}

void audit(AuditEvent event) {
    System.out.printf("request_id=%s action=%s result=%s at=%s%n",
        event.requestId(), event.action(), event.result(), event.at());
}

Übungen

  • Füge Request-IDs in ein Mini-HTTP-Projekt ein.
  • Definiere fünf nützliche Metriken für eine API.
  • Schreibe einen Incident-Review nach einem simulierten Fehler.

Mastery-Check

  • Du unterscheidest Logs, Metriken und Traces.
  • Du loggst mit Kontext.
  • Du vermeidest sensible Daten in Logs.
🧹

Refactoring und Clean Code für Fortgeschrittene

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K27

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Refactoring-Schleife Refactoring-Schleife Test sichern Geruch finden Kleine Änderung Test ausführen Commit Refactoring ist kontrollierte Verbesserung ohne Verhaltensänderung.

Refactoring verbessert Struktur, ohne beobachtbares Verhalten zu ändern. Voraussetzungen sind Tests, kleine Schritte und klare Ziele. Typische Code Smells: lange Methoden, große Klassen, primitive Obsession, Feature Envy, Shotgun Surgery, zyklische Abhängigkeiten und unklare Namen.

Guter Code ist nicht unbedingt kurzer Code. Guter Code macht Entscheidungen sichtbar. Ein paar zusätzliche Value Objects können Lesbarkeit und Sicherheit massiv erhöhen.

Masterregel: Refaktoriere in Mikroschritten. Große Rewrite-Impulse sind oft riskanter als der bestehende Code.

Beispielcode

// Vorher: unklare Primitive
void register(String email, String plan, int months) { ... }

// Nachher: Fachliche Typen
record EmailAddress(String value) {}
record SubscriptionPlan(String code) {}
record SubscriptionPeriod(int months) {}
void register(EmailAddress email, SubscriptionPlan plan, SubscriptionPeriod period) { ... }

Übungen

  • Finde drei primitive Obsession-Stellen.
  • Extrahiere eine Methode mit Fachnamen.
  • Führe ein Refactoring nur mit grünem Test durch.

Mastery-Check

  • Du erkennst Code Smells.
  • Du refaktorisierst in kleinen Schritten.
  • Du änderst Struktur ohne Verhalten zu brechen.
🏦

Großes Master-Beispiel: Event-Sourced Ledger API

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K28

Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.

Event-Sourced Ledger Event-Sourced Ledger HTTP Adapter POST/GET Application Service handle command Aggregate decide/apply Event Store append events Read Model project summary Commands ändern nicht direkt Tabellen, sondern erzeugen fachliche Events.

Das folgende Beispiel kombiniert moderne Java-Konzepte in einem kompakten System. Es ist bewusst didaktisch und nicht als fertige Banking-Software gedacht. Du siehst Records, sealed interfaces, Pattern Matching im Switch, Event Sourcing, optimistische Konkurrenzkontrolle, Virtual Threads und eine kleine HTTP-Grenze.

Architekturidee: Ein Command beschreibt Absicht. Das Aggregate entscheidet anhand seines aktuellen Zustands. Daraus entstehen Events. Der Event Store speichert Events append-only. Ein Read Model projiziert Events in eine lesbare Zusammenfassung.

Warum komplex? Dieses Beispiel zwingt dich, über Invarianten, Fehlerverträge, Versionierung, Nebenläufigkeit und Systemgrenzen nachzudenken. Genau dort beginnt Master-Niveau.

Beispielcode

package com.seb4u.demo.ledger;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * Didaktisches All-in-One-Beispiel:
 * - Records als immutable Value Objects
 * - sealed interfaces fuer Commands und Events
 * - In-Memory Event Store mit Optimistic Concurrency
 * - Aggregate-Rehydration aus Event-Stream
 * - Virtual Threads fuer HTTP Requests
 * - Streams fuer Projektionen / Read Model
 *
 * Start:
 *   javac --release 25 -d . MasterLedgerApp.java
 *   java com.seb4u.demo.ledger.MasterLedgerApp
 */
public final class MasterLedgerApp {

    public static void main(String[] args) throws Exception {
        var store = new InMemoryEventStore();
        var service = new LedgerService(store, new RiskPolicy(BigDecimal.valueOf(10_000)));

        var http = HttpServer.create(new InetSocketAddress(8080), 0);
        http.createContext("/accounts", exchange -> route(exchange, service));
        http.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
        http.start();
        System.out.println("Ledger API läuft auf http://localhost:8080/accounts");
    }

    static void route(HttpExchange ex, LedgerService service) throws IOException {
        try {
            var path = ex.getRequestURI().getPath();
            var method = ex.getRequestMethod();
            if (method.equals("POST") && path.equals("/accounts/demo")) {
                var id = new AccountId("DEMO-" + UUID.randomUUID());
                service.handle(new OpenAccount(id, new Owner("Ada Lovelace")));
                service.handle(new Deposit(id, Money.eur("1250.00")));
                respond(ex, 201, "{\"account\":\"" + id.value() + "\"}");
                return;
            }
            if (method.equals("GET") && path.equals("/accounts/summary")) {
                respond(ex, 200, service.summaryAsJson());
                return;
            }
            respond(ex, 404, "{\"error\":\"not_found\"}");
        } catch (DomainException e) {
            respond(ex, 422, "{\"error\":\"" + e.getMessage() + "\"}");
        } catch (Exception e) {
            respond(ex, 500, "{\"error\":\"internal\"}");
        }
    }

    static void respond(HttpExchange ex, int status, String body) throws IOException {
        var bytes = body.getBytes(StandardCharsets.UTF_8);
        ex.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
        ex.sendResponseHeaders(status, bytes.length);
        try (var out = ex.getResponseBody()) { out.write(bytes); }
    }

    record AccountId(String value) {
        AccountId { if (value == null || value.isBlank()) throw new DomainException("account_id_blank"); }
    }
    record Owner(String name) {
        Owner { if (name == null || name.isBlank()) throw new DomainException("owner_blank"); }
    }
    record Money(BigDecimal amount, Currency currency) implements Comparable<Money> {
        Money {
            Objects.requireNonNull(amount); Objects.requireNonNull(currency);
            amount = amount.setScale(2, java.math.RoundingMode.HALF_UP);
        }
        static Money eur(String value) { return new Money(new BigDecimal(value), Currency.getInstance("EUR")); }
        Money plus(Money other) { sameCurrency(other); return new Money(amount.add(other.amount), currency); }
        Money minus(Money other) { sameCurrency(other); return new Money(amount.subtract(other.amount), currency); }
        boolean negative() { return amount.signum() < 0; }
        private void sameCurrency(Money other) {
            if (!currency.equals(other.currency)) throw new DomainException("currency_mismatch");
        }
        @Override public int compareTo(Money o) { sameCurrency(o); return amount.compareTo(o.amount); }
    }

    sealed interface Command permits OpenAccount, Deposit, Withdraw, Transfer { AccountId accountId(); }
    record OpenAccount(AccountId accountId, Owner owner) implements Command {}
    record Deposit(AccountId accountId, Money amount) implements Command {}
    record Withdraw(AccountId accountId, Money amount) implements Command {}
    record Transfer(AccountId accountId, AccountId target, Money amount) implements Command {}

    sealed interface Event permits AccountOpened, MoneyDeposited, MoneyWithdrawn, TransferRequested, TransferRejected {
        long version(); Instant at(); AccountId accountId();
    }
    record AccountOpened(long version, Instant at, AccountId accountId, Owner owner) implements Event {}
    record MoneyDeposited(long version, Instant at, AccountId accountId, Money amount) implements Event {}
    record MoneyWithdrawn(long version, Instant at, AccountId accountId, Money amount) implements Event {}
    record TransferRequested(long version, Instant at, AccountId accountId, AccountId target, Money amount) implements Event {}
    record TransferRejected(long version, Instant at, AccountId accountId, AccountId target, String reason) implements Event {}

    static final class LedgerService {
        private final InMemoryEventStore store;
        private final RiskPolicy risk;
        LedgerService(InMemoryEventStore store, RiskPolicy risk) { this.store = store; this.risk = risk; }

        List<Event> handle(Command command) {
            var aggregate = Account.rehydrate(command.accountId(), store.eventsOf(command.accountId()));
            var newEvents = switch (command) {
                case OpenAccount c -> aggregate.open(c.owner());
                case Deposit c -> aggregate.deposit(c.amount());
                case Withdraw c -> aggregate.withdraw(c.amount());
                case Transfer c -> risk.allowed(c.amount())
                    ? aggregate.transfer(c.target(), c.amount())
                    : List.of(new TransferRejected(aggregate.nextVersion(), Instant.now(), c.accountId(), c.target(), "risk_limit"));
            };
            store.append(command.accountId(), aggregate.version(), newEvents);
            return newEvents;
        }

        String summaryAsJson() {
            var accounts = store.allEvents().stream()
                .collect(Collectors.groupingBy(Event::accountId))
                .entrySet().stream()
                .map(e -> Account.rehydrate(e.getKey(), e.getValue()))
                .filter(Account::opened)
                .sorted(Comparator.comparing(a -> a.id.value()))
                .map(a -> "{\"id\":\"" + a.id.value() + "\",\"balance\":\"" + a.balance.amount() + " " + a.balance.currency() + "\"}")
                .collect(Collectors.joining(","));
            return "[" + accounts + "]";
        }
    }

    static final class Account {
        private AccountId id;
        private Owner owner;
        private Money balance = Money.eur("0.00");
        private long version;
        boolean opened() { return owner != null; }
        long version() { return version; }
        long nextVersion() { return version + 1; }

        static Account rehydrate(AccountId id, List<Event> history) {
            var acc = new Account();
            acc.id = id;
            history.stream().sorted(Comparator.comparingLong(Event::version)).forEach(acc::apply);
            return acc;
        }
        List<Event> open(Owner owner) {
            require(!opened(), "already_opened");
            return List.of(new AccountOpened(nextVersion(), Instant.now(), id, owner));
        }
        List<Event> deposit(Money amount) {
            require(opened(), "not_opened"); require(!amount.negative(), "negative_deposit");
            return List.of(new MoneyDeposited(nextVersion(), Instant.now(), id, amount));
        }
        List<Event> withdraw(Money amount) {
            require(opened(), "not_opened"); require(balance.compareTo(amount) >= 0, "insufficient_funds");
            return List.of(new MoneyWithdrawn(nextVersion(), Instant.now(), id, amount));
        }
        List<Event> transfer(AccountId target, Money amount) {
            require(opened(), "not_opened"); require(balance.compareTo(amount) >= 0, "insufficient_funds");
            return List.of(
                new TransferRequested(nextVersion(), Instant.now(), id, target, amount),
                new MoneyWithdrawn(nextVersion() + 1, Instant.now(), id, amount)
            );
        }
        private void apply(Event e) {
            switch (e) {
                case AccountOpened ev -> { id = ev.accountId(); owner = ev.owner(); version = ev.version(); }
                case MoneyDeposited ev -> { balance = balance.plus(ev.amount()); version = ev.version(); }
                case MoneyWithdrawn ev -> { balance = balance.minus(ev.amount()); version = ev.version(); }
                case TransferRequested ev -> version = ev.version();
                case TransferRejected ev -> version = ev.version();
            }
        }
    }

    static final class InMemoryEventStore {
        private final Map<AccountId, List<Event>> streams = new HashMap<>();
        private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        List<Event> eventsOf(AccountId id) {
            lock.readLock().lock();
            try { return List.copyOf(streams.getOrDefault(id, List.of())); }
            finally { lock.readLock().unlock(); }
        }
        List<Event> allEvents() {
            lock.readLock().lock();
            try { return streams.values().stream().flatMap(List::stream).toList(); }
            finally { lock.readLock().unlock(); }
        }
        void append(AccountId id, long expectedVersion, List<Event> events) {
            lock.writeLock().lock();
            try {
                var stream = new ArrayList<>(streams.getOrDefault(id, List.of()));
                var actual = stream.isEmpty() ? 0 : stream.getLast().version();
                if (actual != expectedVersion) throw new DomainException("version_conflict");
                stream.addAll(events);
                streams.put(id, List.copyOf(stream));
            } finally { lock.writeLock().unlock(); }
        }
    }

    record RiskPolicy(BigDecimal maxSingleTransfer) {
        boolean allowed(Money money) { return money.amount().compareTo(maxSingleTransfer) <= 0; }
    }
    static final class DomainException extends RuntimeException { DomainException(String msg) { super(msg); } }
    static void require(boolean ok, String msg) { if (!ok) throw new DomainException(msg); }
}

Übungen

  • Erweitere das System um `CloseAccount`.
  • Baue eine Projektion `totalBalanceByCurrency`.
  • Ergänze Idempotency Keys für HTTP-POST.
  • Schreibe Tests für `insufficient_funds` und `version_conflict`.
  • Ersetze In-Memory Store durch JDBC-Store.

Mastery-Check

  • Du kannst Command, Event und Aggregate unterscheiden.
  • Du erkennst, wo Transaktionen nötig sind.
  • Du kannst Virtual Threads sinnvoll einsetzen.
💻

Komplexe Java-Code-Labs

Modul D · Professional Backend, Architektur & Code-Labs

Zusätzliche Deep-Dive-Beispiele für Master-Niveau

Dieser neue Abschnitt ergänzt das Handbuch um umfangreichere, realitätsnahe Java-Beispiele. Die Beispiele sind bewusst architekturlastig: Domänenmodell, Ports und Adapter, Fehlerbehandlung, Event-Sourcing, JDBC, Nebenläufigkeit, Backpressure, Security, Testbarkeit und Observability.

💻

Code-Lab: Hexagonale Modulstruktur

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K31

Komplexes Praxisbeispiel mit modernem Java und Architektur-Fokus

Dieses Beispiel trennt Domäne, Anwendung und Adapter so, dass die fachliche Logik ohne HTTP, SQL oder Frameworks testbar bleibt. Der Code zeigt bewusst nur Standard-Java-Bausteine und eine klare Paket-/Modulgrenze.

Hexagonale Architektur im Code Hexagonale Architektur im Code Domain fachlicher Kern REST Controller CLI Adapter Test Fixtures Repository Port Payment Gateway Clock / UUID Port Adapter kennen die Domäne; die Domäne kennt keine Adapter.

Komplexer Beispielcode

// module-info.java
module com.seb4u.demo.shop {
    exports com.seb4u.demo.shop.domain;
    exports com.seb4u.demo.shop.application;

    requires java.net.http;
    requires java.sql;
}

// src/main/java/com/academy/shop/domain/Order.java
package com.seb4u.demo.shop.domain;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.*;

public final class Order {
    private final OrderId id;
    private final CustomerId customerId;
    private final List<LineItem> items = new ArrayList<>();
    private OrderStatus status = OrderStatus.DRAFT;
    private Instant submittedAt;

    private Order(OrderId id, CustomerId customerId) {
        this.id = Objects.requireNonNull(id);
        this.customerId = Objects.requireNonNull(customerId);
    }

    public static Order draft(OrderId id, CustomerId customerId) {
        return new Order(id, customerId);
    }

    public void addItem(Sku sku, int quantity, Money unitPrice) {
        require(status == OrderStatus.DRAFT, "order_not_editable");
        require(quantity > 0, "quantity_must_be_positive");
        require(unitPrice.isPositive(), "price_must_be_positive");
        items.add(new LineItem(sku, quantity, unitPrice));
    }

    public void submit(Instant now) {
        require(status == OrderStatus.DRAFT, "only_draft_can_be_submitted");
        require(!items.isEmpty(), "empty_order");
        status = OrderStatus.SUBMITTED;
        submittedAt = Objects.requireNonNull(now);
    }

    public Money total() {
        return items.stream()
            .map(LineItem::subtotal)
            .reduce(Money.zero("EUR"), Money::plus);
    }

    public Snapshot snapshot() {
        return new Snapshot(id, customerId, List.copyOf(items), status, submittedAt, total());
    }

    public record Snapshot(OrderId id, CustomerId customerId, List<LineItem> items,
                           OrderStatus status, Instant submittedAt, Money total) {}

    private static void require(boolean ok, String code) {
        if (!ok) throw new DomainRuleViolation(code);
    }
}

record OrderId(String value) { public OrderId { Objects.requireNonNull(value); } }
record CustomerId(String value) { public CustomerId { Objects.requireNonNull(value); } }
record Sku(String value) { public Sku { Objects.requireNonNull(value); } }
enum OrderStatus { DRAFT, SUBMITTED, PAID, FULFILLED, CANCELLED }

record Money(BigDecimal amount, Currency currency) {
    Money {
        Objects.requireNonNull(amount);
        Objects.requireNonNull(currency);
        amount = amount.setScale(2, java.math.RoundingMode.HALF_UP);
    }
    static Money zero(String currency) { return new Money(BigDecimal.ZERO, Currency.getInstance(currency)); }
    boolean isPositive() { return amount.signum() > 0; }
    Money plus(Money other) {
        if (!currency.equals(other.currency)) throw new DomainRuleViolation("currency_mismatch");
        return new Money(amount.add(other.amount), currency);
    }
    Money times(int factor) { return new Money(amount.multiply(BigDecimal.valueOf(factor)), currency); }
}

record LineItem(Sku sku, int quantity, Money unitPrice) {
    Money subtotal() { return unitPrice.times(quantity); }
}

final class DomainRuleViolation extends RuntimeException {
    DomainRuleViolation(String code) { super(code); }
}
💻

Code-Lab: Result, Validation und Fehler ohne Exception-Spaghetti

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K32

Komplexes Praxisbeispiel mit modernem Java und Architektur-Fokus

Große Systeme brauchen saubere Fehlergrenzen. Dieses Beispiel zeigt einen kleinen `Result`-Typ, kombinierbare Validierungen und ein Mapping von fachlichen Problemen auf API-Antworten.

Validation Pipeline Validation Pipeline Raw Input Syntax prüfen Fachregeln Command bauen Use Case Nicht jeder Fehler ist eine Exception. Viele Fehler sind erwartete Ergebnisse.

Komplexer Beispielcode

import java.math.BigDecimal;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Pattern;

sealed interface Result<T> permits Result.Ok, Result.Err {
    record Ok<T>(T value) implements Result<T> {}
    record Err<T>(List<Problem> problems) implements Result<T> {}

    static <T> Result<T> ok(T value) { return new Ok<>(value); }
    static <T> Result<T> err(String field, String code) { return new Err<>(List.of(new Problem(field, code))); }

    default <U> Result<U> map(Function<T, U> mapper) {
        return switch (this) {
            case Ok<T> ok -> ok(mapper.apply(ok.value()));
            case Err<T> err -> new Err<>(err.problems());
        };
    }

    default <U> Result<U> flatMap(Function<T, Result<U>> mapper) {
        return switch (this) {
            case Ok<T> ok -> mapper.apply(ok.value());
            case Err<T> err -> new Err<>(err.problems());
        };
    }

    static <A, B, R> Result<R> combine(Result<A> a, Result<B> b, BiConstructor<A, B, R> ctor) {
        var problems = new ArrayList<Problem>();
        A av = null; B bv = null;
        switch (a) { case Ok<A> ok -> av = ok.value(); case Err<A> err -> problems.addAll(err.problems()); }
        switch (b) { case Ok<B> ok -> bv = ok.value(); case Err<B> err -> problems.addAll(err.problems()); }
        return problems.isEmpty() ? ok(ctor.create(av, bv)) : new Err<>(problems);
    }
}

record Problem(String field, String code) {}
@FunctionalInterface interface BiConstructor<A, B, R> { R create(A a, B b); }

record RegisterCustomer(String email, BigDecimal initialCredit) {}

final class Validators {
    private static final Pattern MAIL = Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$");

    static Result<String> email(String raw) {
        if (raw == null || raw.isBlank()) return Result.err("email", "blank");
        var normalized = raw.trim().toLowerCase(Locale.ROOT);
        if (!MAIL.matcher(normalized).matches()) return Result.err("email", "invalid_format");
        return Result.ok(normalized);
    }

    static Result<BigDecimal> nonNegativeMoney(String raw) {
        try {
            var value = new BigDecimal(raw).setScale(2, java.math.RoundingMode.HALF_UP);
            return value.signum() >= 0 ? Result.ok(value) : Result.err("initialCredit", "negative");
        } catch (RuntimeException ex) {
            return Result.err("initialCredit", "not_a_decimal");
        }
    }

    static Result<RegisterCustomer> registerCustomer(Map<String, String> form) {
        return Result.combine(
            email(form.get("email")),
            nonNegativeMoney(form.getOrDefault("initialCredit", "0")),
            RegisterCustomer::new
        );
    }
}

final class ApiErrorMapper {
    static Map<String, Object> toResponse(Result.Err<?> err) {
        return Map.of(
            "status", 422,
            "title", "Validation failed",
            "problems", err.problems().stream()
                .map(p -> Map.of("field", p.field(), "code", p.code()))
                .toList()
        );
    }
}
💻

Code-Lab: Event-Sourcing mit Snapshots und Projektionen

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K33

Komplexes Praxisbeispiel mit modernem Java und Architektur-Fokus

Dieses Beispiel erweitert das Event-Sourcing-Denken: Events sind die Wahrheit, Snapshots beschleunigen Rehydration, Projektionen liefern schnelle Leseansichten.

Event Sourcing Read/Write Model Event Sourcing Read/Write Model Command Aggregate Events Snapshot Projection Query Schreibmodell und Lesemodell können unterschiedlich optimiert werden.

Komplexer Beispielcode

import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

sealed interface OrderEvent permits OrderOpened, ItemAdded, OrderSubmitted, PaymentCaptured {
    UUID orderId(); long version(); Instant occurredAt();
}
record OrderOpened(UUID orderId, long version, Instant occurredAt, UUID customerId) implements OrderEvent {}
record ItemAdded(UUID orderId, long version, Instant occurredAt, String sku, int quantity, long cents) implements OrderEvent {}
record OrderSubmitted(UUID orderId, long version, Instant occurredAt) implements OrderEvent {}
record PaymentCaptured(UUID orderId, long version, Instant occurredAt, String providerRef) implements OrderEvent {}

record Snapshot(UUID orderId, long version, OrderState state) {}
record OrderState(UUID customerId, List<String> items, boolean submitted, boolean paid, long totalCents) {}

final class OrderAggregate {
    private UUID id;
    private UUID customerId;
    private final List<String> items = new ArrayList<>();
    private boolean submitted;
    private boolean paid;
    private long totalCents;
    private long version;

    static OrderAggregate from(Snapshot snapshot, List<OrderEvent> laterEvents) {
        var aggregate = new OrderAggregate();
        if (snapshot != null) aggregate.restore(snapshot);
        laterEvents.stream()
            .sorted(Comparator.comparingLong(OrderEvent::version))
            .forEach(aggregate::apply);
        return aggregate;
    }

    List<OrderEvent> open(UUID orderId, UUID customerId) {
        require(id == null, "already_opened");
        return List.of(new OrderOpened(orderId, version + 1, Instant.now(), customerId));
    }

    List<OrderEvent> addItem(String sku, int quantity, long cents) {
        require(id != null, "not_opened");
        require(!submitted, "already_submitted");
        require(quantity > 0 && cents > 0, "invalid_item");
        return List.of(new ItemAdded(id, version + 1, Instant.now(), sku, quantity, cents));
    }

    List<OrderEvent> submit() {
        require(!items.isEmpty(), "empty_order");
        require(!submitted, "already_submitted");
        return List.of(new OrderSubmitted(id, version + 1, Instant.now()));
    }

    List<OrderEvent> capturePayment(String providerRef) {
        require(submitted, "not_submitted");
        require(!paid, "already_paid");
        return List.of(new PaymentCaptured(id, version + 1, Instant.now(), providerRef));
    }

    Snapshot snapshot() {
        return new Snapshot(id, version, new OrderState(customerId, List.copyOf(items), submitted, paid, totalCents));
    }

    private void restore(Snapshot s) {
        id = s.orderId(); version = s.version(); customerId = s.state().customerId();
        items.clear(); items.addAll(s.state().items());
        submitted = s.state().submitted(); paid = s.state().paid(); totalCents = s.state().totalCents();
    }

    private void apply(OrderEvent event) {
        switch (event) {
            case OrderOpened e -> { id = e.orderId(); customerId = e.customerId(); version = e.version(); }
            case ItemAdded e -> { items.add(e.sku() + " x" + e.quantity()); totalCents += e.quantity() * e.cents(); version = e.version(); }
            case OrderSubmitted e -> { submitted = true; version = e.version(); }
            case PaymentCaptured e -> { paid = true; version = e.version(); }
        }
    }

    private static void require(boolean ok, String code) { if (!ok) throw new IllegalStateException(code); }
}

final class SalesProjection {
    private final Map<UUID, Long> revenueByCustomer = new ConcurrentHashMap<>();
    private final Set<UUID> paidOrders = ConcurrentHashMap.newKeySet();

    void on(OrderEvent event) {
        if (event instanceof PaymentCaptured paid && paidOrders.add(paid.orderId())) {
            // In echten Systemen holt die Projektion die Summe aus einem Order-Readmodell oder Event-Stream.
            revenueByCustomer.merge(UUID.fromString("00000000-0000-0000-0000-000000000001"), 1L, Long::sum);
        }
    }

    Map<UUID, Long> revenueByCustomer() { return Map.copyOf(revenueByCustomer); }
}

final class VersionedEventStore {
    private final Map<UUID, List<OrderEvent>> streams = new ConcurrentHashMap<>();
    private final AtomicLong globalSequence = new AtomicLong();

    synchronized void append(UUID streamId, long expectedVersion, List<OrderEvent> events) {
        var current = new ArrayList<>(streams.getOrDefault(streamId, List.of()));
        var actual = current.isEmpty() ? 0 : current.getLast().version();
        if (actual != expectedVersion) throw new IllegalStateException("optimistic_lock_failed");
        globalSequence.addAndGet(events.size());
        current.addAll(events);
        streams.put(streamId, List.copyOf(current));
    }
}
💻

Code-Lab: JDBC Repository mit Transaktion und Optimistic Locking

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K34

Komplexes Praxisbeispiel mit modernem Java und Architektur-Fokus

Hier siehst du ein repository-nahes Beispiel ohne ORM-Magie. Wichtig sind klare Transaktionsgrenzen, Prepared Statements, Versionen und ein Row Mapper, der SQL von Domänenlogik trennt.

JDBC Write Flow JDBC Write Flow Use Case Connection SELECT version UPDATE ... WHERE version Commit Domain Event Optimistic Locking verhindert verlorene Updates ohne globale Sperren.

Komplexer Beispielcode

import javax.sql.DataSource;
import java.sql.*;
import java.util.Optional;
import java.util.UUID;

record Customer(UUID id, String email, int version, boolean active) {}

final class CustomerRepository {
    private final DataSource dataSource;
    CustomerRepository(DataSource dataSource) { this.dataSource = dataSource; }

    Optional<Customer> findById(Connection con, UUID id) throws SQLException {
        try (var ps = con.prepareStatement("""
            select id, email, version, active
            from customers
            where id = ?
            """)) {
            ps.setObject(1, id);
            try (var rs = ps.executeQuery()) {
                return rs.next() ? Optional.of(map(rs)) : Optional.empty();
            }
        }
    }

    void save(Connection con, Customer customer, int expectedVersion) throws SQLException {
        try (var ps = con.prepareStatement("""
            update customers
            set email = ?, active = ?, version = version + 1
            where id = ? and version = ?
            """)) {
            ps.setString(1, customer.email());
            ps.setBoolean(2, customer.active());
            ps.setObject(3, customer.id());
            ps.setInt(4, expectedVersion);
            int updated = ps.executeUpdate();
            if (updated != 1) throw new OptimisticLockException(customer.id());
        }
    }

    <T> T inTransaction(SqlWork<T> work) {
        try (var con = dataSource.getConnection()) {
            boolean oldAutoCommit = con.getAutoCommit();
            con.setAutoCommit(false);
            try {
                T result = work.execute(con);
                con.commit();
                return result;
            } catch (Exception ex) {
                con.rollback();
                throw new RepositoryException("transaction_failed", ex);
            } finally {
                con.setAutoCommit(oldAutoCommit);
            }
        } catch (SQLException ex) {
            throw new RepositoryException("connection_failed", ex);
        }
    }

    void deactivate(UUID id) {
        inTransaction(con -> {
            var customer = findById(con, id).orElseThrow(() -> new RepositoryException("not_found"));
            save(con, new Customer(customer.id(), customer.email(), customer.version(), false), customer.version());
            insertOutbox(con, id, "CustomerDeactivated");
            return null;
        });
    }

    private void insertOutbox(Connection con, UUID aggregateId, String type) throws SQLException {
        try (var ps = con.prepareStatement("""
            insert into outbox_events(id, aggregate_id, type, payload, created_at)
            values (?, ?, ?, ?, current_timestamp)
            """)) {
            ps.setObject(1, UUID.randomUUID());
            ps.setObject(2, aggregateId);
            ps.setString(3, type);
            ps.setString(4, "{}");
            ps.executeUpdate();
        }
    }

    private Customer map(ResultSet rs) throws SQLException {
        return new Customer(
            rs.getObject("id", UUID.class),
            rs.getString("email"),
            rs.getInt("version"),
            rs.getBoolean("active")
        );
    }
}

@FunctionalInterface interface SqlWork<T> { T execute(Connection con) throws Exception; }
final class OptimisticLockException extends RuntimeException { OptimisticLockException(UUID id) { super("version_conflict:" + id); } }
final class RepositoryException extends RuntimeException {
    RepositoryException(String message) { super(message); }
    RepositoryException(String message, Throwable cause) { super(message, cause); }
}
💻

Code-Lab: Virtual Threads, Bulkhead und kontrollierte Parallelität

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K35

Komplexes Praxisbeispiel mit modernem Java und Architektur-Fokus

Virtual Threads erlauben viele blockierende Aufgaben, aber unbegrenzte Parallelität bleibt gefährlich. Dieses Beispiel kombiniert Virtual Threads mit Semaphore, Timeout und Fehlerreport.

Robuster Importer Robuster Importer CSV Zeilen Semaphore Virtual Thread HTTP/DB Call Result sammeln Report Virtual Threads lösen nicht automatisch Rate Limits, DB-Pools oder Backpressure.

Komplexer Beispielcode

import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

record ImportRow(String externalId, String payload) {}
record ImportResult(String externalId, boolean success, String message) {}

final class BulkheadedImporter implements AutoCloseable {
    private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
    private final Semaphore bulkhead;
    private final Duration timeout;
    private final RemoteClient client;

    BulkheadedImporter(int maxConcurrentCalls, Duration timeout, RemoteClient client) {
        this.bulkhead = new Semaphore(maxConcurrentCalls);
        this.timeout = timeout;
        this.client = client;
    }

    List<ImportResult> importAll(List<ImportRow> rows) {
        var futures = rows.stream()
            .map(row -> CompletableFuture.supplyAsync(() -> importOne(row), executor)
                .orTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS)
                .exceptionally(ex -> new ImportResult(row.externalId(), false, rootMessage(ex))))
            .toList();

        return futures.stream().map(CompletableFuture::join).toList();
    }

    private ImportResult importOne(ImportRow row) {
        boolean acquired = false;
        try {
            bulkhead.acquire();
            acquired = true;
            var response = client.send(row.payload());
            return new ImportResult(row.externalId(), response.accepted(), response.message());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return new ImportResult(row.externalId(), false, "interrupted");
        } catch (RuntimeException e) {
            return new ImportResult(row.externalId(), false, rootMessage(e));
        } finally {
            if (acquired) bulkhead.release();
        }
    }

    ImportSummary summarize(List<ImportResult> results) {
        var ok = new AtomicInteger();
        var failed = new AtomicInteger();
        var errors = new TreeMap<String, Integer>();

        results.forEach(r -> {
            if (r.success()) ok.incrementAndGet();
            else {
                failed.incrementAndGet();
                errors.merge(r.message(), 1, Integer::sum);
            }
        });
        return new ImportSummary(ok.get(), failed.get(), Map.copyOf(errors));
    }

    private static String rootMessage(Throwable t) {
        Throwable x = t;
        while (x.getCause() != null) x = x.getCause();
        return x.getClass().getSimpleName() + ":" + Objects.toString(x.getMessage(), "no_message");
    }

    @Override public void close() { executor.close(); }
}

record ImportSummary(int success, int failed, Map<String, Integer> errorsByType) {}
record RemoteResponse(boolean accepted, String message) {}
interface RemoteClient { RemoteResponse send(String payload); }
💻

Code-Lab: CompletableFuture-Orchestrierung mit Fallbacks

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K36

Komplexes Praxisbeispiel mit modernem Java und Architektur-Fokus

Dieses Muster ist nützlich, wenn ein Use Case mehrere unabhängige Datenquellen aggregiert. Du siehst Timeouts, Fallbacks, Fehlergrenzen und ein klares Ergebnisobjekt.

Async Aggregation Async Aggregation Request User API Pricing API Inventory API Combine Fallback Parallelisierung ist nur sinnvoll, wenn Fehler- und Timeout-Verhalten definiert ist.

Komplexer Beispielcode

import java.math.BigDecimal;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Supplier;

record ProductPage(String sku, String title, BigDecimal price, int available, List<String> warnings) {}
record UserContext(String locale, boolean premium) {}
record Price(BigDecimal value) { static Price unknown() { return new Price(BigDecimal.valueOf(-1)); } }
record Stock(int available) { static Stock unknown() { return new Stock(0); } }

final class ProductPageAssembler {
    private final Executor executor;
    private final UserApi userApi;
    private final PriceApi priceApi;
    private final InventoryApi inventoryApi;

    ProductPageAssembler(Executor executor, UserApi userApi, PriceApi priceApi, InventoryApi inventoryApi) {
        this.executor = executor;
        this.userApi = userApi;
        this.priceApi = priceApi;
        this.inventoryApi = inventoryApi;
    }

    CompletableFuture<ProductPage> assemble(String sku, UUID userId) {
        var warnings = Collections.synchronizedList(new ArrayList<String>());

        var user = call("user", () -> userApi.context(userId), new UserContext("de-DE", false), warnings);
        var price = call("price", () -> priceApi.priceFor(sku), Price.unknown(), warnings);
        var stock = call("stock", () -> inventoryApi.stockFor(sku), Stock.unknown(), warnings);

        return user.thenCombine(price, (u, p) -> Map.entry(u, p))
            .thenCombine(stock, (entry, s) -> new ProductPage(
                sku,
                localizedTitle(sku, entry.getKey().locale()),
                entry.getValue().value(),
                s.available(),
                List.copyOf(warnings)
            ));
    }

    private <T> CompletableFuture<T> call(String name, Supplier<T> supplier, T fallback, List<String> warnings) {
        return CompletableFuture.supplyAsync(supplier, executor)
            .completeOnTimeout(fallback, 700, TimeUnit.MILLISECONDS)
            .exceptionally(ex -> {
                warnings.add(name + "_fallback:" + ex.getClass().getSimpleName());
                return fallback;
            });
    }

    private String localizedTitle(String sku, String locale) {
        return switch (locale) {
            case "de-DE" -> "Produkt " + sku;
            case "tr-TR" -> "Ürün " + sku;
            default -> "Product " + sku;
        };
    }
}

interface UserApi { UserContext context(UUID userId); }
interface PriceApi { Price priceFor(String sku); }
interface InventoryApi { Stock stockFor(String sku); }
💻

Code-Lab: Flow API mit Backpressure

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K37

Komplexes Praxisbeispiel mit modernem Java und Architektur-Fokus

Java enthält mit `java.util.concurrent.Flow` eine standardisierte Basis für Publisher/Subscriber. Dieses Beispiel ist klein, zeigt aber die wichtigste Idee: Der Subscriber fordert bewusst Daten an.

Flow Backpressure Flow Backpressure Publisher Subscription request(n) onNext onError/onComplete Backpressure bedeutet: Konsumenten bestimmen, wie viel sie verarbeiten können.

Komplexer Beispielcode

import java.util.*;
import java.util.concurrent.Flow.*;
import java.util.concurrent.SubmissionPublisher;
import java.util.concurrent.atomic.AtomicInteger;

record DomainNotification(String type, String payload) {}

final class NotificationBus implements AutoCloseable {
    private final SubmissionPublisher<DomainNotification> publisher = new SubmissionPublisher<>();

    void publish(DomainNotification notification) {
        int lag = publisher.submit(notification);
        if (lag > 1_000) {
            System.err.println("warning: subscriber lag is high: " + lag);
        }
    }

    void subscribe(Subscriber<DomainNotification> subscriber) {
        publisher.subscribe(subscriber);
    }

    @Override public void close() { publisher.close(); }
}

final class BatchingSubscriber implements Subscriber<DomainNotification> {
    private final int batchSize;
    private final List<DomainNotification> buffer = new ArrayList<>();
    private final AtomicInteger processed = new AtomicInteger();
    private Subscription subscription;

    BatchingSubscriber(int batchSize) { this.batchSize = batchSize; }

    @Override public void onSubscribe(Subscription subscription) {
        this.subscription = subscription;
        subscription.request(batchSize);
    }

    @Override public void onNext(DomainNotification item) {
        buffer.add(item);
        if (buffer.size() == batchSize) flush();
        if (processed.incrementAndGet() % batchSize == 0) subscription.request(batchSize);
    }

    @Override public void onError(Throwable throwable) {
        System.err.println("stream failed: " + throwable.getMessage());
    }

    @Override public void onComplete() {
        flush();
        System.out.println("stream complete");
    }

    private void flush() {
        if (buffer.isEmpty()) return;
        System.out.println("Persisting batch: " + buffer);
        buffer.clear();
    }
}
💻

Code-Lab: Type-safe Builder mit Staged Interfaces

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K38

Komplexes Praxisbeispiel mit modernem Java und Architektur-Fokus

Ein normaler Builder erlaubt oft ungültige Zwischenzustände. Dieser Staged Builder erzwingt per Typ-System, dass Pflichtfelder gesetzt werden, bevor `build()` sichtbar wird.

Staged Builder Staged Builder customer() shipping() billing() addItem() build() Das Typ-System wird zur API-Dokumentation.

Komplexer Beispielcode

import java.util.*;

record Checkout(UUID customerId, Address shipping, Address billing, List<String> skuList) {}
record Address(String street, String city, String country) {}

final class CheckoutBuilder {
    public interface CustomerStage { ShippingStage customer(UUID customerId); }
    public interface ShippingStage { BillingStage shipping(Address address); }
    public interface BillingStage { ItemStage billing(Address address); }
    public interface ItemStage {
        ItemStage addItem(String sku);
        Checkout build();
    }

    public static CustomerStage start() { return new Steps(); }

    private static final class Steps implements CustomerStage, ShippingStage, BillingStage, ItemStage {
        private UUID customerId;
        private Address shipping;
        private Address billing;
        private final List<String> skuList = new ArrayList<>();

        @Override public ShippingStage customer(UUID customerId) {
            this.customerId = Objects.requireNonNull(customerId);
            return this;
        }

        @Override public BillingStage shipping(Address address) {
            this.shipping = Objects.requireNonNull(address);
            return this;
        }

        @Override public ItemStage billing(Address address) {
            this.billing = Objects.requireNonNull(address);
            return this;
        }

        @Override public ItemStage addItem(String sku) {
            if (sku == null || sku.isBlank()) throw new IllegalArgumentException("sku_blank");
            skuList.add(sku);
            return this;
        }

        @Override public Checkout build() {
            if (skuList.isEmpty()) throw new IllegalStateException("checkout_without_items");
            return new Checkout(customerId, shipping, billing, List.copyOf(skuList));
        }
    }

    static Checkout example() {
        return CheckoutBuilder.start()
            .customer(UUID.randomUUID())
            .shipping(new Address("Main Street 1", "Vienna", "AT"))
            .billing(new Address("Main Street 1", "Vienna", "AT"))
            .addItem("JAVA-BOOK")
            .addItem("JVM-POSTER")
            .build();
    }
}
💻

Code-Lab: Specification Pattern und Query DSL

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K39

Komplexes Praxisbeispiel mit modernem Java und Architektur-Fokus

Dieses Beispiel zeigt, wie du Filterlogik komponierbar machst. Die gleiche Specification kann für In-Memory-Tests genutzt und später auf SQL/Criteria übersetzt werden.

Composable Query Logic Composable Query Logic Spec A and() Spec B or() Predicate Repository Fachliche Filterregeln sollten nicht überall als if-Blöcke verstreut sein.

Komplexer Beispielcode

import java.math.BigDecimal;
import java.time.Instant;
import java.util.*;
import java.util.function.Predicate;

record Invoice(UUID id, UUID customerId, BigDecimal amount, boolean paid, Instant dueAt) {}

@FunctionalInterface
interface Specification<T> {
    boolean isSatisfiedBy(T item);

    default Specification<T> and(Specification<T> other) {
        return item -> this.isSatisfiedBy(item) && other.isSatisfiedBy(item);
    }

    default Specification<T> or(Specification<T> other) {
        return item -> this.isSatisfiedBy(item) || other.isSatisfiedBy(item);
    }

    default Specification<T> not() {
        return item -> !this.isSatisfiedBy(item);
    }

    default Predicate<T> asPredicate() { return this::isSatisfiedBy; }
}

final class InvoiceSpecs {
    static Specification<Invoice> unpaid() { return invoice -> !invoice.paid(); }
    static Specification<Invoice> overdue(Instant now) { return invoice -> invoice.dueAt().isBefore(now); }
    static Specification<Invoice> amountAtLeast(BigDecimal min) { return invoice -> invoice.amount().compareTo(min) >= 0; }
    static Specification<Invoice> customer(UUID customerId) { return invoice -> invoice.customerId().equals(customerId); }
}

final class InvoiceRepository {
    private final List<Invoice> invoices;
    InvoiceRepository(List<Invoice> invoices) { this.invoices = List.copyOf(invoices); }

    List<Invoice> findAll(Specification<Invoice> specification, Comparator<Invoice> order) {
        return invoices.stream()
            .filter(specification.asPredicate())
            .sorted(order)
            .toList();
    }

    List<Invoice> riskyReceivables(Instant now) {
        var spec = InvoiceSpecs.unpaid()
            .and(InvoiceSpecs.overdue(now))
            .and(InvoiceSpecs.amountAtLeast(BigDecimal.valueOf(1_000)));

        return findAll(spec, Comparator.comparing(Invoice::amount).reversed());
    }
}
💻

Code-Lab: Passwort-Hashing mit Standard-JDK

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K40

Komplexes Praxisbeispiel mit modernem Java und Architektur-Fokus

Für Security-Beispiele ist wichtig: niemals Klartext speichern, niemals selbst Kryptografie erfinden. Dieses Beispiel verwendet PBKDF2 aus dem JDK und kapselt Parameter sichtbar.

Password Storage Password Storage Passwort Salt PBKDF2 Hash Parameter speichern Konstant vergleichen Security-Code braucht klare Defaults und leichte Austauschbarkeit.

Komplexer Beispielcode

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.*;
import java.util.*;

record PasswordHash(String algorithm, int iterations, int saltBytes, String saltBase64, String hashBase64) {}

final class PasswordHasher {
    private static final String ALGORITHM = "PBKDF2WithHmacSHA256";
    private static final int ITERATIONS = 210_000;
    private static final int SALT_BYTES = 16;
    private static final int HASH_BITS = 256;
    private final SecureRandom random = new SecureRandom();

    PasswordHash hash(char[] password) {
        byte[] salt = new byte[SALT_BYTES];
        random.nextBytes(salt);
        byte[] hash = pbkdf2(password, salt, ITERATIONS, HASH_BITS);
        return new PasswordHash(
            ALGORITHM,
            ITERATIONS,
            SALT_BYTES,
            Base64.getEncoder().encodeToString(salt),
            Base64.getEncoder().encodeToString(hash)
        );
    }

    boolean verify(char[] password, PasswordHash stored) {
        byte[] salt = Base64.getDecoder().decode(stored.saltBase64());
        byte[] expected = Base64.getDecoder().decode(stored.hashBase64());
        byte[] actual = pbkdf2(password, salt, stored.iterations(), expected.length * 8);
        return MessageDigest.isEqual(expected, actual);
    }

    private byte[] pbkdf2(char[] password, byte[] salt, int iterations, int hashBits) {
        try {
            var spec = new PBEKeySpec(password, salt, iterations, hashBits);
            var factory = SecretKeyFactory.getInstance(ALGORITHM);
            return factory.generateSecret(spec).getEncoded();
        } catch (Exception e) {
            throw new IllegalStateException("password_hash_failed", e);
        } finally {
            Arrays.fill(password, '\0');
        }
    }
}
💻

Code-Lab: Testbare Zeit, IDs und Ports

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K41

Komplexes Praxisbeispiel mit modernem Java und Architektur-Fokus

Gute Tests entstehen durch kontrollierbare Abhängigkeiten. Dieses Beispiel zeigt Ports für Zeit und IDs, damit Tests deterministisch bleiben.

Deterministische Tests Deterministische Tests Use Case Clock Port Id Port Fake Adapter Assertion Zufall und Zeit gehören hinter Schnittstellen, nicht mitten in die Fachlogik.

Komplexer Beispielcode

import java.time.*;
import java.util.*;

interface IdGenerator { UUID next(); }
interface TimeProvider { Instant now(); }

record Ticket(UUID id, String title, Instant createdAt, TicketPriority priority) {}
enum TicketPriority { LOW, NORMAL, HIGH }

final class CreateTicketUseCase {
    private final IdGenerator ids;
    private final TimeProvider time;
    private final TicketRepository repository;

    CreateTicketUseCase(IdGenerator ids, TimeProvider time, TicketRepository repository) {
        this.ids = ids; this.time = time; this.repository = repository;
    }

    Ticket create(String title, TicketPriority priority) {
        if (title == null || title.isBlank()) throw new IllegalArgumentException("title_blank");
        var ticket = new Ticket(ids.next(), title.trim(), time.now(), priority == null ? TicketPriority.NORMAL : priority);
        repository.save(ticket);
        return ticket;
    }
}

interface TicketRepository { void save(Ticket ticket); Optional<Ticket> find(UUID id); }

final class InMemoryTicketRepository implements TicketRepository {
    private final Map<UUID, Ticket> data = new LinkedHashMap<>();
    public void save(Ticket ticket) { data.put(ticket.id(), ticket); }
    public Optional<Ticket> find(UUID id) { return Optional.ofNullable(data.get(id)); }
}

final class CreateTicketUseCaseTestStyle {
    static void testCreatesTicketDeterministically() {
        var fixedId = UUID.fromString("00000000-0000-0000-0000-000000000123");
        var fixedTime = Instant.parse("2026-07-02T10:15:30Z");
        var repo = new InMemoryTicketRepository();
        var useCase = new CreateTicketUseCase(() -> fixedId, () -> fixedTime, repo);

        var ticket = useCase.create(" Production bug ", TicketPriority.HIGH);

        assertEquals(fixedId, ticket.id());
        assertEquals("Production bug", ticket.title());
        assertEquals(fixedTime, ticket.createdAt());
        assertTrue(repo.find(fixedId).isPresent());
    }

    private static void assertEquals(Object expected, Object actual) {
        if (!Objects.equals(expected, actual)) throw new AssertionError("expected=" + expected + ", actual=" + actual);
    }
    private static void assertTrue(boolean value) { if (!value) throw new AssertionError("expected true"); }
}
💻

Code-Lab: Strukturierte Logs und einfache Metriken ohne Framework

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K42

Komplexes Praxisbeispiel mit modernem Java und Architektur-Fokus

Auch ohne Framework kannst du produktionsnahe Signale erzeugen: korrelierbare Logs, Dauer-Messungen, Zähler und saubere Fehlerkontexte.

Observability Basics Observability Basics Request-ID Timer Use Case Log Event Metric Alert-Grundlage Was du nicht messen kannst, kannst du im Betrieb kaum verbessern.

Komplexer Beispielcode

import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Supplier;

record RequestContext(String requestId, String userId) {}

final class StructuredLogger {
    void info(String event, RequestContext ctx, Map<String, ?> fields) {
        log("INFO", event, ctx, fields, null);
    }

    void error(String event, RequestContext ctx, Map<String, ?> fields, Throwable error) {
        log("ERROR", event, ctx, fields, error);
    }

    private void log(String level, String event, RequestContext ctx, Map<String, ?> fields, Throwable error) {
        var all = new LinkedHashMap<String, Object>();
        all.put("level", level);
        all.put("event", event);
        all.put("requestId", ctx.requestId());
        all.put("userId", ctx.userId());
        all.put("at", Instant.now().toString());
        all.putAll(fields);
        if (error != null) all.put("error", error.getClass().getSimpleName() + ":" + error.getMessage());
        System.out.println(toJsonLine(all));
    }

    private String toJsonLine(Map<String, ?> map) {
        return map.entrySet().stream()
            .map(e -> "\"" + e.getKey() + "\":\"" + String.valueOf(e.getValue()).replace("\"", "'") + "\"")
            .reduce("{", (a, b) -> a.equals("{") ? a + b : a + "," + b) + "}";
    }
}

final class Metrics {
    private final Map<String, LongAdder> counters = new ConcurrentHashMap<>();
    void increment(String name) { counters.computeIfAbsent(name, ignored -> new LongAdder()).increment(); }
    Map<String, Long> snapshot() {
        var result = new TreeMap<String, Long>();
        counters.forEach((key, value) -> result.put(key, value.sum()));
        return result;
    }
}

final class ObservedUseCaseRunner {
    private final StructuredLogger log;
    private final Metrics metrics;

    ObservedUseCaseRunner(StructuredLogger log, Metrics metrics) {
        this.log = log; this.metrics = metrics;
    }

    <T> T run(String name, RequestContext ctx, Supplier<T> useCase) {
        var start = Instant.now();
        try {
            T result = useCase.get();
            var duration = Duration.between(start, Instant.now()).toMillis();
            metrics.increment(name + ".success");
            log.info(name + ".success", ctx, Map.of("durationMs", duration));
            return result;
        } catch (RuntimeException ex) {
            var duration = Duration.between(start, Instant.now()).toMillis();
            metrics.increment(name + ".failure");
            log.error(name + ".failure", ctx, Map.of("durationMs", duration), ex);
            throw ex;
        }
    }
}

Zusätzliche SVG-Lernbilder

Modul D · Professional Backend, Architektur & Code-Labs

Diese Galerie enthält zusätzliche visuelle Merkbilder für Wiederholung, Präsentationen und Spickzettel.

🧩

Code-Atlas pro Thema: mehrere komplexe Beispielcodes

Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K43

Zusätzliche, direkt kopierbare Java-Beispiele zu jedem Hauptthema

Dieser Abschnitt erweitert das Handbuch um einen Code-Atlas: Jedes Hauptthema bekommt mehrere zusätzliche Beispiele. Die Beispiele sind bewusst nicht nur „Hello World“, sondern zeigen produktionsnahe Muster: klare Invarianten, Ports, Transaktionen, Nebenläufigkeit, Fehlerverträge, Metriken, Sicherheit und testbare Architektur.

Alle Beispiele bleiben in dieser einen HTML-Datei. Die Codeblöcke werden offline mit Java-Syntax-Highlighting dargestellt und können über den Button kopiert werden.

Code-Atlas: pro Thema mehrere produktionsnahe Beispiele FundamentSyntax · Typen · OOP DatenflussCollections · Streams · I/O SystemgrenzenHTTP · JDBC · Security ProduktionPerformance · Logs · Tests Jedes Thema enthält mindestens zwei zusätzliche Codebeispiele mit Erklärung. Nutze die Suche links: z.B. „Bulkhead“, „Outbox“, „Value Object“, „TopologicalSort“, „HMAC“.

🧩 1. Toolchain, JDK und produktive CLI

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Toolchain-Gate: Version, Vendor und Release prüfen

Ein kleines Build-Gate, das vor CI-Schritten sicherstellt, dass die richtige Java-Laufzeit verwendet wird.

import java.util.*;
import java.util.stream.*;

public final class ToolchainGate {
    record Requirement(int major, Set<String> allowedVendors) {}

    public static void main(String[] args) {
        var req = new Requirement(25, Set.of("Oracle", "Eclipse Adoptium", "OpenJDK"));
        Runtime.Version version = Runtime.version();
        String vendor = System.getProperty("java.vendor", "unknown");
        boolean majorOk = version.feature() >= req.major();
        boolean vendorOk = req.allowedVendors().stream().anyMatch(vendor::contains);
        if (!majorOk || !vendorOk) {
            throw new IllegalStateException("Wrong Java: " + version + " / " + vendor);
        }
        Map<String, String> report = Map.of(
            "java", version.toString(),
            "vendor", vendor,
            "modules", String.valueOf(ModuleLayer.boot().modules().size())
        );
        report.forEach((k, v) -> System.out.printf("%-8s %s%n", k + ":", v));
    }
}
ProcessBuilder: javac/java als reproduzierbarer Task

Nützlich für eigene Lern-Runner, kleine Build-Tools und Diagnoseprogramme ohne externe Libraries.

import java.io.*;
import java.nio.file.*;
import java.util.*;

public final class MiniBuildRunner {
    static int run(Path dir, List<String> command) throws IOException, InterruptedException {
        Process process = new ProcessBuilder(command)
            .directory(dir.toFile())
            .redirectErrorStream(true)
            .start();
        try (var out = process.inputReader()) {
            out.lines().forEach(line -> System.out.println("[tool] " + line));
        }
        return process.waitFor();
    }

    public static void main(String[] args) throws Exception {
        Path project = Path.of(args.length == 0 ? "." : args[0]).toAbsolutePath();
        List<String> compile = List.of("javac", "--release", "25", "-d", "out", "src/App.java");
        List<String> run = List.of("java", "-cp", "out", "App");
        if (run(project, compile) != 0) throw new IllegalStateException("compile failed");
        if (run(project, run) != 0) throw new IllegalStateException("program failed");
    }
}

🧩 2. Quellcode, Bytecode und Laufzeitmodell

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Startreihenfolge sichtbar machen

Zeigt Class Loading, statische Initialisierung, Instanzinitialisierung und Ressourcenlebensdauer.

public class RuntimeOrderLab {
    static final Trace TRACE = new Trace("static field");
    static { System.out.println("static block"); }
    { System.out.println("instance initializer"); }

    RuntimeOrderLab() { System.out.println("constructor"); }

    public static void main(String[] args) throws Exception {
        System.out.println("main enters");
        try (Trace ignored = new Trace("try-with-resources")) {
            new RuntimeOrderLab().work();
        }
        System.out.println("main exits");
    }

    void work() { System.out.println("virtual call"); }
    record Trace(String name) implements AutoCloseable {
        Trace { System.out.println("open " + name); }
        public void close() { System.out.println("close " + name); }
    }
}
Service-Loader mental: Plug-ins dynamisch laden

Ein runtime-nahes Muster für entkoppelte Erweiterungen über Interfaces.

import java.util.*;

public class PluginRuntime {
    public interface CommandPlugin {
        String name();
        int run(List<String> args);
    }

    static final class BuiltInHello implements CommandPlugin {
        public String name() { return "hello"; }
        public int run(List<String> args) {
            System.out.println("Hello " + String.join(" ", args));
            return 0;
        }
    }

    public static void main(String[] args) {
        List<CommandPlugin> plugins = new ArrayList<>();
        plugins.add(new BuiltInHello());
        ServiceLoader.load(CommandPlugin.class).forEach(plugins::add);
        String selected = args.length == 0 ? "hello" : args[0];
        CommandPlugin plugin = plugins.stream()
            .filter(p -> p.name().equals(selected))
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("unknown command " + selected));
        System.exit(plugin.run(Arrays.asList(args).subList(Math.min(1, args.length), args.length)));
    }
}

🧩 3. Syntax, Werte, Typen und Invarianten

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Value Objects statt primitive obsession

Records validieren ihre Invarianten direkt im Konstruktor und machen Fachlogik sichtbar.

import java.math.*;
import java.util.*;

record Money(BigDecimal amount, Currency currency) {
    Money {
        Objects.requireNonNull(amount);
        Objects.requireNonNull(currency);
        amount = amount.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_UP);
    }
    Money plus(Money other) {
        requireSameCurrency(other);
        return new Money(amount.add(other.amount), currency);
    }
    Money multiply(BigDecimal factor) { return new Money(amount.multiply(factor), currency); }
    private void requireSameCurrency(Money other) {
        if (!currency.equals(other.currency)) throw new IllegalArgumentException("currency mismatch");
    }
    static Money eur(String value) { return new Money(new BigDecimal(value), Currency.getInstance("EUR")); }
}
Parser mit sealed Result

Statt null oder magischer Werte: ein Parser liefert typisierte Erfolge und Fehler.

sealed interface ParseResult<T> permits ParseResult.Ok, ParseResult.Invalid {
    record Ok<T>(T value) implements ParseResult<T> {}
    record Invalid<T>(String input, String reason) implements ParseResult<T> {}
}

record CustomerNumber(String value) {
    CustomerNumber {
        if (!value.matches("CUST-[0-9]{6}")) throw new IllegalArgumentException(value);
    }
    static ParseResult<CustomerNumber> parse(String raw) {
        String normalized = raw == null ? "" : raw.trim().toUpperCase();
        if (!normalized.matches("CUST-[0-9]{6}")) {
            return new ParseResult.Invalid<>(raw, "expected CUST-000000 pattern");
        }
        return new ParseResult.Ok<>(new CustomerNumber(normalized));
    }
}

🧩 4. Kontrollfluss, State Machines und Fehlerdenken

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Explizite State Machine für Bestellungen

Komplexer Kontrollfluss wird als Übergangstabelle modelliert, nicht als if-Spaghetti.

import java.util.*;

enum OrderState { DRAFT, PLACED, PAID, SHIPPED, CANCELLED }
enum OrderAction { PLACE, PAY, SHIP, CANCEL }

record Transition(OrderState from, OrderAction action) {}

final class OrderWorkflow {
    private final Map<Transition, OrderState> transitions = Map.of(
        new Transition(OrderState.DRAFT, OrderAction.PLACE), OrderState.PLACED,
        new Transition(OrderState.PLACED, OrderAction.PAY), OrderState.PAID,
        new Transition(OrderState.PAID, OrderAction.SHIP), OrderState.SHIPPED,
        new Transition(OrderState.DRAFT, OrderAction.CANCEL), OrderState.CANCELLED,
        new Transition(OrderState.PLACED, OrderAction.CANCEL), OrderState.CANCELLED
    );
    OrderState apply(OrderState current, OrderAction action) {
        return Optional.ofNullable(transitions.get(new Transition(current, action)))
            .orElseThrow(() -> new IllegalStateException("illegal transition: " + current + " -> " + action));
    }
}
Guard Clauses mit Audit Trail

Fehlerentscheidungen werden gesammelt und auditierbar statt früh unklar abgebrochen.

import java.util.*;

record Decision(boolean allowed, List<String> reasons) {
    static Decision allow() { return new Decision(true, List.of()); }
    static Decision deny(String reason) { return new Decision(false, List.of(reason)); }
    Decision and(Decision other) {
        var all = new ArrayList<String>();
        all.addAll(reasons); all.addAll(other.reasons);
        return new Decision(allowed && other.allowed, List.copyOf(all));
    }
}

final class CreditGuard {
    Decision canApprove(Customer c, Order o) {
        return hasVerifiedEmail(c)
            .and(hasPositiveCredit(c))
            .and(orderBelowLimit(c, o));
    }
    Decision hasVerifiedEmail(Customer c) { return c.verifiedEmail() ? Decision.allow() : Decision.deny("email not verified"); }
    Decision hasPositiveCredit(Customer c) { return c.creditScore() > 600 ? Decision.allow() : Decision.deny("low score"); }
    Decision orderBelowLimit(Customer c, Order o) { return o.total() <= c.limit() ? Decision.allow() : Decision.deny("limit exceeded"); }
    record Customer(boolean verifiedEmail, int creditScore, long limit) {}
    record Order(long total) {}
}

🧩 5. Methoden, API-Design und Verträge

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Such-API mit stabilen Parametern

Ein gutes API-Objekt verhindert Parameterlisten, ungültige Defaults und unlesbare Aufrufe.

import java.time.*;
import java.util.*;

record PageRequest(int page, int size) {
    PageRequest {
        if (page < 0 || size < 1 || size > 200) throw new IllegalArgumentException("invalid page");
    }
}
record Sort(String field, Direction direction) { enum Direction { ASC, DESC } }
record OrderSearch(String customerId, Instant from, Instant to, PageRequest page, List<Sort> sort) {
    OrderSearch {
        Objects.requireNonNull(page);
        sort = List.copyOf(sort == null ? List.of() : sort);
        if (from != null && to != null && from.isAfter(to)) throw new IllegalArgumentException("from after to");
    }
    static OrderSearch firstPageForCustomer(String id) {
        return new OrderSearch(id, null, null, new PageRequest(0, 50), List.of(new Sort("createdAt", Sort.Direction.DESC)));
    }
}
Command Handler mit klarer Methodengrenze

Die öffentliche Methode validiert und orchestriert; private Methoden bleiben klein und fachlich benannt.

import java.util.*;

final class RegisterCustomerHandler {
    private final CustomerRepository repository;
    RegisterCustomerHandler(CustomerRepository repository) { this.repository = repository; }

    CustomerId handle(RegisterCustomer command) {
        validate(command);
        ensureEmailIsFree(command.email());
        Customer customer = Customer.register(command.name(), command.email());
        repository.save(customer);
        return customer.id();
    }
    private void validate(RegisterCustomer c) {
        if (c.name().isBlank() || !c.email().contains("@")) throw new IllegalArgumentException("invalid customer");
    }
    private void ensureEmailIsFree(String email) {
        repository.findByEmail(email).ifPresent(c -> { throw new IllegalStateException("email taken"); });
    }
    record RegisterCustomer(String name, String email) {}
    record CustomerId(UUID value) {}
    record Customer(CustomerId id, String name, String email) { static Customer register(String n, String e) { return new Customer(new CustomerId(UUID.randomUUID()), n, e); } }
    interface CustomerRepository { Optional<Customer> findByEmail(String email); void save(Customer customer); }
}

🧩 6. Objektorientierung und Domänenmodelle

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Polymorphie ohne instanceof

Neue Rabattarten werden ergänzt, ohne bestehende Berechnungslogik anzufassen.

import java.math.*;
import java.util.*;

interface DiscountPolicy { Money discountFor(Cart cart); }
record PercentDiscount(BigDecimal percent) implements DiscountPolicy {
    public Money discountFor(Cart cart) { return cart.subtotal().multiply(percent.movePointLeft(2)); }
}
record FixedDiscount(Money amount) implements DiscountPolicy {
    public Money discountFor(Cart cart) { return amount; }
}
record Cart(List<Line> lines, DiscountPolicy policy) {
    Cart { lines = List.copyOf(lines); }
    Money subtotal() { return lines.stream().map(Line::total).reduce(Money.eur("0"), Money::plus); }
    Money total() { return subtotal().minus(policy.discountFor(this)); }
}
record Line(String sku, int quantity, Money price) { Money total() { return price.multiply(BigDecimal.valueOf(quantity)); } }
Komposition statt fragile Basisklasse

Verhalten wird über kleine Rollen komponiert; das reduziert Vererbungskopplung.

import java.time.*;
import java.util.*;

interface Auditable { Audit audit(); }
interface SoftDeletable { boolean deleted(); }
record Audit(String createdBy, Instant createdAt, String changedBy, Instant changedAt) {}
record Product(UUID id, String sku, String name, Audit audit, boolean deleted) implements Auditable, SoftDeletable {}

final class ProductService {
    Product rename(Product product, String newName, String user, Clock clock) {
        if (product.deleted()) throw new IllegalStateException("deleted product cannot change");
        Audit old = product.audit();
        Audit changed = new Audit(old.createdBy(), old.createdAt(), user, Instant.now(clock));
        return new Product(product.id(), product.sku(), newName.strip(), changed, false);
    }
}

🧩 7. Records, Enums, sealed Types und Pattern Matching

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Sealed Commands für vollständige Verarbeitung

Der Compiler hilft, alle bekannten Command-Typen im switch zu behandeln.

import java.math.*;

sealed interface AccountCommand permits OpenAccount, Deposit, Withdraw, CloseAccount {}
record OpenAccount(String owner) implements AccountCommand {}
record Deposit(BigDecimal amount) implements AccountCommand {}
record Withdraw(BigDecimal amount) implements AccountCommand {}
record CloseAccount(String reason) implements AccountCommand {}

final class CommandPrinter {
    static String describe(AccountCommand command) {
        return switch (command) {
            case OpenAccount c -> "open for " + c.owner();
            case Deposit c -> "deposit " + c.amount();
            case Withdraw c -> "withdraw " + c.amount();
            case CloseAccount c -> "close: " + c.reason();
        };
    }
}
Enum als kleine Strategie

Enums können mehr sein als Konstanten: hier kapseln sie Gebührenlogik.

import java.math.*;

enum TransferRail {
    SEPA { BigDecimal fee(BigDecimal amount) { return new BigDecimal("0.10"); } },
    SWIFT { BigDecimal fee(BigDecimal amount) { return amount.multiply(new BigDecimal("0.0020")).max(new BigDecimal("8.00")); } },
    INTERNAL { BigDecimal fee(BigDecimal amount) { return BigDecimal.ZERO; } };
    abstract BigDecimal fee(BigDecimal amount);
}
record Transfer(BigDecimal amount, TransferRail rail) {
    BigDecimal totalDebit() { return amount.add(rail.fee(amount)); }
}

🧩 8. Collections und Datenstrukturen

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
LRU-Cache mit TTL

LinkedHashMap kann als kleine, gut kontrollierbare Cache-Struktur verwendet werden.

import java.time.*;
import java.util.*;

final class TtlLruCache<K, V> {
    record Entry<V>(V value, Instant expiresAt) {}
    private final int maxSize;
    private final Duration ttl;
    private final Clock clock;
    private final Map<K, Entry<V>> map;
    TtlLruCache(int maxSize, Duration ttl, Clock clock) {
        this.maxSize = maxSize; this.ttl = ttl; this.clock = clock;
        this.map = new LinkedHashMap<>(16, .75f, true) {
            protected boolean removeEldestEntry(Map.Entry<K, Entry<V>> eldest) { return size() > TtlLruCache.this.maxSize; }
        };
    }
    synchronized Optional<V> get(K key) {
        Entry<V> e = map.get(key);
        if (e == null || e.expiresAt().isBefore(Instant.now(clock))) { map.remove(key); return Optional.empty(); }
        return Optional.of(e.value());
    }
    synchronized void put(K key, V value) { map.put(key, new Entry<>(value, Instant.now(clock).plus(ttl))); }
}
Topologische Sortierung für Abhängigkeiten

Ein Klassiker für Build-Reihenfolgen, Migrationen und Workflow-Schritte.

import java.util.*;

final class TopologicalSort {
    static List<String> sort(Map<String, Set<String>> dependsOn) {
        Map<String, Integer> indegree = new HashMap<>();
        Map<String, Set<String>> outgoing = new HashMap<>();
        dependsOn.forEach((node, deps) -> {
            indegree.putIfAbsent(node, 0);
            for (String dep : deps) {
                outgoing.computeIfAbsent(dep, k -> new HashSet<>()).add(node);
                indegree.merge(node, 1, Integer::sum);
                indegree.putIfAbsent(dep, 0);
            }
        });
        ArrayDeque<String> ready = new ArrayDeque<>();
        indegree.forEach((n, d) -> { if (d == 0) ready.add(n); });
        List<String> result = new ArrayList<>();
        while (!ready.isEmpty()) {
            String n = ready.removeFirst(); result.add(n);
            for (String m : outgoing.getOrDefault(n, Set.of())) if (indegree.merge(m, -1, Integer::sum) == 0) ready.add(m);
        }
        if (result.size() != indegree.size()) throw new IllegalStateException("cycle detected");
        return result;
    }
}

🧩 9. Generics und Typsicherheit

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Typed IDs verhindern ID-Verwechslung

Generics und Records sorgen dafür, dass CustomerId und OrderId nicht versehentlich gemischt werden.

import java.util.*;

interface Entity<ID> { ID id(); }
record CustomerId(UUID value) {}
record OrderId(UUID value) {}
record Customer(CustomerId id, String name) implements Entity<CustomerId> {}
record Order(OrderId id, CustomerId customerId) implements Entity<OrderId> {}

interface Repository<ID, E extends Entity<ID>> {
    Optional<E> find(ID id);
    void save(E entity);
}
final class InMemoryRepository<ID, E extends Entity<ID>> implements Repository<ID, E> {
    private final Map<ID, E> store = new HashMap<>();
    public Optional<E> find(ID id) { return Optional.ofNullable(store.get(id)); }
    public void save(E entity) { store.put(entity.id(), entity); }
}
Codec-Combinators

Ein generisches Codec-Pattern verbindet Validierung, Parsing und Serialisierung.

import java.util.function.*;

interface Codec<T> {
    String encode(T value);
    T decode(String raw);
    default <R> Codec<R> map(Function<T, R> from, Function<R, T> to) {
        Codec<T> self = this;
        return new Codec<>() {
            public String encode(R value) { return self.encode(to.apply(value)); }
            public R decode(String raw) { return from.apply(self.decode(raw)); }
        };
    }
    static Codec<Integer> integer() { return new Codec<>() { public String encode(Integer v) { return v.toString(); } public Integer decode(String r) { return Integer.parseInt(r); } }; }
}
record Age(int value) { Age { if (value < 0 || value > 130) throw new IllegalArgumentException(); } }
Codec<Age> ageCodec = Codec.integer().map(Age::new, Age::value);

🧩 10. Lambdas, Streams und funktionales Denken

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Eigener Collector für Revenue-Analytics

Ein Collector kapselt Aggregation und macht Pipelines fachlicher.

import java.math.*;
import java.util.*;
import java.util.stream.*;

record Sale(String region, String product, BigDecimal amount) {}
record RevenueStats(long count, BigDecimal total, BigDecimal max) {}

final class RevenueCollector {
    static Collector<Sale, ?, RevenueStats> stats() {
        class Acc { long count; BigDecimal total = BigDecimal.ZERO; BigDecimal max = BigDecimal.ZERO; }
        return Collector.of(
            Acc::new,
            (a, s) -> { a.count++; a.total = a.total.add(s.amount()); a.max = a.max.max(s.amount()); },
            (a, b) -> { a.count += b.count; a.total = a.total.add(b.total); a.max = a.max.max(b.max); return a; },
            a -> new RevenueStats(a.count, a.total, a.max)
        );
    }
}
Pipeline mit teeing und immutable Ergebnis

Mehrere Kennzahlen entstehen in einem lesbaren Stream-Ausdruck.

import java.math.*;
import java.util.*;
import java.util.stream.*;

record Invoice(String customer, BigDecimal amount, boolean paid) {}
record Dashboard(long paidCount, BigDecimal openAmount) {}

final class InvoiceDashboard {
    static Dashboard dashboard(List<Invoice> invoices) {
        return invoices.stream().collect(Collectors.teeing(
            Collectors.filtering(Invoice::paid, Collectors.counting()),
            Collectors.filtering(i -> !i.paid(), Collectors.mapping(Invoice::amount,
                Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))),
            Dashboard::new
        ));
    }
}

🧩 11. Exceptions und robuste Fehlerverträge

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Fehler in API-Responses mappen

Domain-Fehler bleiben fachlich, technische Responses entstehen erst am Rand des Systems.

sealed class AppException extends RuntimeException permits NotFound, Conflict, ValidationFailed {
    AppException(String message) { super(message); }
}
final class NotFound extends AppException { NotFound(String m) { super(m); } }
final class Conflict extends AppException { Conflict(String m) { super(m); } }
final class ValidationFailed extends AppException { ValidationFailed(String m) { super(m); } }
record ErrorResponse(int status, String code, String message) {}

final class ErrorMapper {
    static ErrorResponse map(Throwable t) {
        return switch (t) {
            case NotFound e -> new ErrorResponse(404, "NOT_FOUND", e.getMessage());
            case Conflict e -> new ErrorResponse(409, "CONFLICT", e.getMessage());
            case ValidationFailed e -> new ErrorResponse(422, "VALIDATION", e.getMessage());
            default -> new ErrorResponse(500, "INTERNAL", "unexpected error");
        };
    }
}
Retry mit Backoff und checked Exceptions

Robustheit entsteht durch begrenzte, beobachtbare Wiederholung statt Endlosschleifen.

import java.time.*;

@FunctionalInterface interface ThrowingSupplier<T> { T get() throws Exception; }

final class Retry {
    static <T> T withBackoff(int attempts, Duration delay, ThrowingSupplier<T> action) throws Exception {
        Exception last = null;
        for (int i = 1; i <= attempts; i++) {
            try { return action.get(); }
            catch (Exception e) {
                last = e;
                if (i == attempts) break;
                Thread.sleep(delay.multipliedBy(i));
            }
        }
        throw last;
    }
}

🧩 12. I/O, Dateien, NIO.2 und Ressourcen

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Sicherer Import mit temporärer Datei

Schreibe erst atomar und ersetze dann, damit keine halben Dateien sichtbar werden.

import java.io.*;
import java.nio.file.*;
import java.util.*;

final class AtomicFileWriter {
    static void writeAtomically(Path target, List<String> lines) throws IOException {
        Path dir = target.toAbsolutePath().getParent();
        Files.createDirectories(dir);
        Path temp = Files.createTempFile(dir, target.getFileName().toString(), ".tmp");
        try {
            Files.write(temp, lines, StandardOpenOption.TRUNCATE_EXISTING);
            Files.move(temp, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
        } finally {
            Files.deleteIfExists(temp);
        }
    }
}
Directory Watcher mit Debounce

Ein WatchService reagiert auf Änderungen, ohne jeden Schreibschritt einzeln zu verarbeiten.

import java.nio.file.*;
import java.time.*;
import java.util.*;

final class DebouncedWatcher {
    static void watch(Path dir, Duration quietPeriod) throws Exception {
        try (WatchService ws = FileSystems.getDefault().newWatchService()) {
            dir.register(ws, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY);
            Map<Path, Instant> changed = new HashMap<>();
            while (true) {
                WatchKey key = ws.poll(quietPeriod.toMillis(), java.util.concurrent.TimeUnit.MILLISECONDS);
                if (key != null) {
                    for (WatchEvent<?> e : key.pollEvents()) changed.put(dir.resolve((Path) e.context()), Instant.now());
                    key.reset();
                }
                Instant cutoff = Instant.now().minus(quietPeriod);
                changed.entrySet().removeIf(e -> { if (e.getValue().isBefore(cutoff)) { System.out.println("process " + e.getKey()); return true; } return false; });
            }
        }
    }
}

🧩 13. Datum, Zeit, Locale und Text

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Abrechnung über Zeitzonen hinweg

Instant für Speicherung, ZonedDateTime für Geschäftsregeln, LocalDate für Kalenderlogik.

import java.time.*;

record BillingWindow(Instant start, Instant end) {
    static BillingWindow monthly(YearMonth month, ZoneId zone) {
        ZonedDateTime from = month.atDay(1).atStartOfDay(zone);
        ZonedDateTime to = month.plusMonths(1).atDay(1).atStartOfDay(zone);
        return new BillingWindow(from.toInstant(), to.toInstant());
    }
    boolean contains(Instant instant) { return !instant.isBefore(start) && instant.isBefore(end); }
}
Locale-aware Textausgabe

Darstellung gehört an den Rand; Domänenobjekte behalten neutrale Werte.

import java.math.*;
import java.text.*;
import java.time.*;
import java.time.format.*;
import java.util.*;

record Receipt(BigDecimal amount, Currency currency, LocalDate date) {}
final class ReceiptFormatter {
    static String format(Receipt r, Locale locale) {
        NumberFormat money = NumberFormat.getCurrencyInstance(locale);
        money.setCurrency(r.currency());
        DateTimeFormatter date = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(locale);
        return money.format(r.amount()) + " bezahlt am " + date.format(r.date());
    }
}

🧩 14. Nebenläufigkeit und Virtual Threads

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Virtual Threads mit Bulkhead

Viele blockierende Tasks, aber begrenzter Zugriff auf ein knappes Fremdsystem.

import java.util.*;
import java.util.concurrent.*;

final class BulkheadDownloader {
    private final Semaphore remoteLimit = new Semaphore(20);

    List<String> downloadAll(List<String> urls) throws Exception {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<Future<String>> futures = urls.stream().map(url -> executor.submit(() -> guardedDownload(url))).toList();
            List<String> result = new ArrayList<>();
            for (Future<String> f : futures) result.add(f.get());
            return result;
        }
    }
    private String guardedDownload(String url) throws Exception {
        remoteLimit.acquire();
        try { return "body of " + url; }
        finally { remoteLimit.release(); }
    }
}
Producer/Consumer mit Poison Pill

Ein sauberes Shutdown-Protokoll verhindert hängende Worker.

import java.util.concurrent.*;

sealed interface Work permits Job, Stop {}
record Job(String id, Runnable task) implements Work {}
record Stop() implements Work {}

final class WorkerPool {
    private final BlockingQueue<Work> queue = new LinkedBlockingQueue<>();
    void start(int workers) {
        for (int i = 0; i < workers; i++) Thread.ofVirtual().start(this::workerLoop);
    }
    void submit(Job job) { queue.add(job); }
    void stop(int workers) { for (int i = 0; i < workers; i++) queue.add(new Stop()); }
    private void workerLoop() {
        try {
            while (true) switch (queue.take()) { case Job j -> j.task().run(); case Stop s -> { return; } }
        } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

🧩 15. CompletableFuture und asynchrone Workflows

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Asynchrone Komposition mit Fallback

Mehrere Quellen werden parallel geladen; ein optionaler Fallback verhindert Totalausfall.

import java.time.*;
import java.util.concurrent.*;

record Profile(String id, String name) {}
record Recommendations(String text) { static Recommendations empty() { return new Recommendations("none"); } }
record HomePage(Profile profile, Recommendations recommendations) {}

final class HomePageAssembler {
    private final Executor io = Executors.newVirtualThreadPerTaskExecutor();
    CompletableFuture<HomePage> load(String userId) {
        CompletableFuture<Profile> profile = CompletableFuture.supplyAsync(() -> loadProfile(userId), io)
            .orTimeout(500, TimeUnit.MILLISECONDS);
        CompletableFuture<Recommendations> recs = CompletableFuture.supplyAsync(() -> loadRecs(userId), io)
            .completeOnTimeout(Recommendations.empty(), 300, TimeUnit.MILLISECONDS);
        return profile.thenCombine(recs, HomePage::new);
    }
    Profile loadProfile(String id) { return new Profile(id, "Ada"); }
    Recommendations loadRecs(String id) { return new Recommendations("learn Java"); }
}
Async Circuit Breaker

Ein einfacher Schutzmechanismus öffnet nach Fehlern und schließt nach Cooldown wieder.

import java.time.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.function.*;

final class AsyncCircuitBreaker {
    private final AtomicInteger failures = new AtomicInteger();
    private volatile Instant openUntil = Instant.EPOCH;
    <T> CompletableFuture<T> call(Supplier<CompletableFuture<T>> remote) {
        if (Instant.now().isBefore(openUntil)) return CompletableFuture.failedFuture(new IllegalStateException("circuit open"));
        return remote.get().whenComplete((ok, err) -> {
            if (err == null) failures.set(0);
            else if (failures.incrementAndGet() >= 3) openUntil = Instant.now().plusSeconds(10);
        });
    }
}

🧩 16. Speicher, Heap und Garbage Collection

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Bounded Cache statt Speicherleck

Eine klare Größenbegrenzung ist oft wirksamer als späteres GC-Tuning.

import java.util.*;

final class BoundedCache<K, V> {
    private final Map<K, V> map;
    BoundedCache(int maxEntries) {
        this.map = new LinkedHashMap<>(16, .75f, true) {
            protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > maxEntries; }
        };
    }
    synchronized V computeIfAbsent(K key, java.util.function.Function<K, V> loader) {
        return map.computeIfAbsent(key, loader);
    }
    synchronized int size() { return map.size(); }
}
Allocation-Snapshot mit Runtime-Metriken

Kein Ersatz für Profiler, aber gut als Lerninstrument und Smoke-Test.

final class MemorySnapshot {
    record Snapshot(long used, long free, long total, long max) {}
    static Snapshot take() {
        Runtime r = Runtime.getRuntime();
        return new Snapshot(r.totalMemory() - r.freeMemory(), r.freeMemory(), r.totalMemory(), r.maxMemory());
    }
    static void printDelta(String label, Snapshot before, Snapshot after) {
        long delta = after.used() - before.used();
        System.out.printf("%s used %+d KB, total %d MB%n", label, delta / 1024, after.total() / 1024 / 1024);
    }
}

🧩 17. Module, Packages und Architekturgrenzen

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
module-info mit expliziten Ports

Module zeigen Abhängigkeiten hart und sichtbar, statt sie in Packages zu verstecken.

// module-info.java
module com.seb4u.demo.billing {
    exports com.seb4u.demo.billing.api;
    exports com.seb4u.demo.billing.spi;
    requires java.sql;
    uses com.seb4u.demo.billing.spi.TaxProvider;
    provides com.seb4u.demo.billing.spi.TaxProvider with com.seb4u.demo.billing.internal.DefaultTaxProvider;
}
ServiceLoader als Modul-Erweiterung

Adapters können später ergänzt werden, ohne den Kern neu zu kompilieren.

import java.util.*;

package com.seb4u.demo.billing.api;
public interface TaxProvider { String country(); java.math.BigDecimal taxRate(); }

package com.seb4u.demo.billing.internal;
public final class TaxRegistry {
    private final Map<String, TaxProvider> providers;
    public TaxRegistry() {
        providers = ServiceLoader.load(TaxProvider.class).stream()
            .map(ServiceLoader.Provider::get)
            .collect(java.util.stream.Collectors.toUnmodifiableMap(TaxProvider::country, p -> p));
    }
    public TaxProvider forCountry(String country) { return Optional.ofNullable(providers.get(country)).orElseThrow(); }
}

🧩 18. Build, Dependencies und reproduzierbare Projekte

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
BuildInfo aus Manifest lesen

Runtime-Diagnose wird einfacher, wenn Version, Commit und Build-Zeit im Artefakt stecken.

import java.io.*;
import java.util.jar.*;

record BuildInfo(String version, String commit, String builtAt) {
    static BuildInfo fromManifest() {
        try (InputStream in = BuildInfo.class.getResourceAsStream("/META-INF/MANIFEST.MF")) {
            if (in == null) return new BuildInfo("dev", "local", "unknown");
            Attributes a = new Manifest(in).getMainAttributes();
            return new BuildInfo(a.getValue("Implementation-Version"), a.getValue("Git-Commit"), a.getValue("Build-Time"));
        } catch (IOException e) {
            throw new IllegalStateException("manifest unreadable", e);
        }
    }
}
Maven-Enforcer-Idee als Java-Check

Ein eigener Check kann CI-Regeln ergänzen, zum Beispiel verbotene Snapshot-Abhängigkeiten.

import java.nio.file.*;
import java.util.regex.*;

final class PomPolicyCheck {
    static final Pattern SNAPSHOT = Pattern.compile("<version>[^<]*-SNAPSHOT</version>");
    public static void main(String[] args) throws Exception {
        String pom = Files.readString(Path.of("pom.xml"));
        if (SNAPSHOT.matcher(pom).find()) {
            throw new IllegalStateException("SNAPSHOT dependencies are forbidden on main branch");
        }
        if (!pom.contains("<maven.compiler.release>25</maven.compiler.release>")) {
            throw new IllegalStateException("compiler release must be pinned");
        }
    }
}

🧩 19. Testing von Unit bis Contract

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Contract-Test als Interface

Jede Repository-Implementierung muss denselben Verhaltensvertrag erfüllen.

import org.junit.jupiter.api.*;
import java.util.*;

interface CustomerRepositoryContract {
    CustomerRepository repository();

    @Test default void savedCustomerCanBeLoadedAgain() {
        Customer c = new Customer(UUID.randomUUID(), "Ada");
        repository().save(c);
        Assertions.assertEquals(Optional.of(c), repository().find(c.id()));
    }

    @Test default void unknownCustomerIsEmpty() {
        Assertions.assertTrue(repository().find(UUID.randomUUID()).isEmpty());
    }
    record Customer(UUID id, String name) {}
    interface CustomerRepository { void save(Customer c); Optional<Customer> find(UUID id); }
}
Testbare Zeit und deterministische IDs

Ports für Clock und ID-Generator machen Tests reproduzierbar.

import org.junit.jupiter.api.*;
import java.time.*;
import java.util.*;

final class TokenServiceTest {
    @Test void tokenContainsStableExpiry() {
        Clock clock = Clock.fixed(Instant.parse("2026-01-01T00:00:00Z"), ZoneOffset.UTC);
        Ids ids = () -> UUID.fromString("00000000-0000-0000-0000-000000000001");
        TokenService service = new TokenService(clock, ids);
        Assertions.assertEquals("00000000-0000-0000-0000-000000000001:2026-01-01T01:00:00Z", service.issue());
    }
    interface Ids { UUID next(); }
    record TokenService(Clock clock, Ids ids) {
        String issue() { return ids.next() + ":" + Instant.now(clock).plusSeconds(3600); }
    }
}

🧩 20. HTTP, REST, JSON und Systemgrenzen

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Minimaler HTTP-Router mit JDK-Server

Für Lernprojekte reicht oft der eingebaute Server, um Routing und Boundaries zu verstehen.

import com.sun.net.httpserver.*;
import java.io.*;
import java.net.*;
import java.nio.charset.*;
import java.util.*;

final class MiniRouter {
    private final Map<String, Handler> routes = new HashMap<>();
    interface Handler { Response handle(Request request) throws Exception; }
    record Request(String method, URI uri, String body) {}
    record Response(int status, String body) {}

    void get(String path, Handler h) { routes.put("GET " + path, h); }
    HttpHandler toHttpHandler() { return exchange -> {
        String key = exchange.getRequestMethod() + " " + exchange.getRequestURI().getPath();
        Handler h = routes.getOrDefault(key, r -> new Response(404, "not found"));
        String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
        Response r = h.handle(new Request(exchange.getRequestMethod(), exchange.getRequestURI(), body));
        byte[] out = r.body().getBytes(StandardCharsets.UTF_8);
        exchange.sendResponseHeaders(r.status(), out.length);
        exchange.getResponseBody().write(out); exchange.close();
    }; }
}
Resilienter HttpClient-Aufruf

Timeouts, Statusprüfung und Fehlertexte gehören in einen Adapter, nicht in die Domäne.

import java.net.*;
import java.net.http.*;
import java.time.*;

final class RemoteCatalogClient {
    private final HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build();
    String fetchProduct(String sku) throws Exception {
        HttpRequest req = HttpRequest.newBuilder(URI.create("https://catalog.example/products/" + URLEncoder.encode(sku, java.nio.charset.StandardCharsets.UTF_8)))
            .timeout(Duration.ofSeconds(3))
            .header("Accept", "application/json")
            .GET().build();
        HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
        if (res.statusCode() == 404) throw new IllegalArgumentException("unknown sku " + sku);
        if (res.statusCode() / 100 != 2) throw new IllegalStateException("catalog failed: " + res.statusCode());
        return res.body();
    }
}

🧩 21. Datenbanken, JDBC und Transaktionen

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
TransactionTemplate ohne Framework

Transaktionen werden zentral gehandhabt, damit Repositories fachlich bleiben.

import javax.sql.DataSource;
import java.sql.*;

final class TransactionTemplate {
    private final DataSource dataSource;
    TransactionTemplate(DataSource dataSource) { this.dataSource = dataSource; }
    <T> T inTransaction(SqlFunction<Connection, T> work) throws SQLException {
        try (Connection con = dataSource.getConnection()) {
            boolean old = con.getAutoCommit();
            con.setAutoCommit(false);
            try { T result = work.apply(con); con.commit(); return result; }
            catch (Exception e) { con.rollback(); throw e; }
            finally { con.setAutoCommit(old); }
        }
    }
    @FunctionalInterface interface SqlFunction<T, R> { R apply(T value) throws SQLException; }
}
Outbox Pattern mit JDBC

Domänenänderung und Integrationsereignis werden in einer DB-Transaktion gespeichert.

import java.sql.*;
import java.util.*;

final class OrderRepository {
    void saveOrderAndOutbox(Connection con, Order order, String eventJson) throws SQLException {
        try (PreparedStatement orderStmt = con.prepareStatement("insert into orders(id,total) values(?,?)")) {
            orderStmt.setObject(1, order.id());
            orderStmt.setBigDecimal(2, order.total());
            orderStmt.executeUpdate();
        }
        try (PreparedStatement outbox = con.prepareStatement("insert into outbox(id,type,payload) values(?,?,?)")) {
            outbox.setObject(1, UUID.randomUUID());
            outbox.setString(2, "OrderPlaced");
            outbox.setString(3, eventJson);
            outbox.executeUpdate();
        }
    }
    record Order(UUID id, java.math.BigDecimal total) {}
}

🧩 22. Sicherheit: Eingaben, Secrets und Rechte

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Path Traversal verhindern

User-Eingaben werden normalisiert und müssen im erlaubten Basisverzeichnis bleiben.

import java.nio.file.*;

final class SafeFileAccess {
    private final Path base;
    SafeFileAccess(Path base) { this.base = base.toAbsolutePath().normalize(); }
    Path resolveUserPath(String userInput) {
        Path resolved = base.resolve(userInput).normalize();
        if (!resolved.startsWith(base)) throw new SecurityException("path escape detected");
        return resolved;
    }
}
HMAC-Signatur mit Standard-JDK

Tokens werden signiert, ohne geheime Werte im Klartext zu loggen oder zu speichern.

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.*;
import java.util.*;

final class HmacSigner {
    private final byte[] secret;
    HmacSigner(byte[] secret) { this.secret = secret.clone(); }
    String sign(String payload) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(secret, "HmacSHA256"));
            return Base64.getUrlEncoder().withoutPadding().encodeToString(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception e) { throw new IllegalStateException("cannot sign", e); }
    }
    boolean verify(String payload, String signature) { return MessageDigest.isEqual(sign(payload).getBytes(StandardCharsets.UTF_8), signature.getBytes(StandardCharsets.UTF_8)); }
}

🧩 23. Design Patterns ohne Dogma

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Chain of Responsibility für Validierung

Regeln bleiben erweiterbar und einzeln testbar.

import java.util.*;

interface Rule<T> { Optional<String> check(T value); }
final class Validator<T> {
    private final List<Rule<T>> rules;
    Validator(List<Rule<T>> rules) { this.rules = List.copyOf(rules); }
    List<String> validate(T value) { return rules.stream().map(r -> r.check(value)).flatMap(Optional::stream).toList(); }
}
record Signup(String email, String password) {}
Validator<Signup> signupValidator = new Validator<>(List.of(
    s -> s.email().contains("@") ? Optional.empty() : Optional.of("email invalid"),
    s -> s.password().length() >= 12 ? Optional.empty() : Optional.of("password too short")
));
Decorator für Caching und Logging

Cross-cutting Verhalten wird um einen Port gelegt, ohne die Kernimplementierung zu verschmutzen.

import java.util.*;

interface ProductPort { Product load(String sku); }
record Product(String sku, String name) {}
final class CachedProductPort implements ProductPort {
    private final ProductPort delegate;
    private final Map<String, Product> cache = new HashMap<>();
    CachedProductPort(ProductPort delegate) { this.delegate = delegate; }
    public synchronized Product load(String sku) { return cache.computeIfAbsent(sku, delegate::load); }
}
final class LoggingProductPort implements ProductPort {
    private final ProductPort delegate;
    LoggingProductPort(ProductPort delegate) { this.delegate = delegate; }
    public Product load(String sku) { System.out.println("load sku=" + sku); return delegate.load(sku); }
}

🧩 24. DDD und hexagonale Architektur

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Use Case mit Ports

Der Kern kennt nur Interfaces. Adapter für DB, HTTP oder Messaging bleiben außen.

import java.util.*;

record AccountId(UUID value) {}
record OpenAccount(String owner) {}
interface AccountRepository { void save(Account account); }
interface DomainEvents { void publish(Object event); }
record Account(AccountId id, String owner) { static Account open(String owner) { return new Account(new AccountId(UUID.randomUUID()), owner); } }
record AccountOpened(AccountId id, String owner) {}

final class OpenAccountUseCase {
    private final AccountRepository repository;
    private final DomainEvents events;
    OpenAccountUseCase(AccountRepository repository, DomainEvents events) { this.repository = repository; this.events = events; }
    AccountId handle(OpenAccount command) {
        Account account = Account.open(command.owner());
        repository.save(account);
        events.publish(new AccountOpened(account.id(), account.owner()));
        return account.id();
    }
}
Aggregate schützt Invarianten

Fachregeln sitzen dort, wo der Zustand lebt.

import java.math.*;
import java.util.*;

final class Wallet {
    private final UUID id;
    private BigDecimal balance;
    private boolean frozen;
    Wallet(UUID id) { this.id = id; this.balance = BigDecimal.ZERO; }
    void deposit(BigDecimal amount) { requirePositive(amount); balance = balance.add(amount); }
    void withdraw(BigDecimal amount) {
        requirePositive(amount);
        if (frozen) throw new IllegalStateException("wallet frozen");
        if (balance.compareTo(amount) < 0) throw new IllegalStateException("insufficient funds");
        balance = balance.subtract(amount);
    }
    void freeze() { frozen = true; }
    private static void requirePositive(BigDecimal amount) { if (amount.signum() <= 0) throw new IllegalArgumentException("amount"); }
}

🧩 25. Performance messen, nicht raten

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Kleines Benchmark-Harness

Für echte Messungen nutzt man JMH; dieses Harness zeigt nur Grundprinzipien: Warmup, Wiederholung, Blackhole.

import java.util.function.*;

final class TinyBenchmark {
    static volatile Object blackhole;
    static long measure(String name, int warmup, int iterations, Supplier<?> work) {
        for (int i = 0; i < warmup; i++) blackhole = work.get();
        long start = System.nanoTime();
        for (int i = 0; i < iterations; i++) blackhole = work.get();
        long nanos = System.nanoTime() - start;
        System.out.printf("%s: %.2f ns/op%n", name, (double) nanos / iterations);
        return nanos;
    }
}
Algorithmuswahl sichtbar machen

O(n) schlägt O(n²): Datenstrukturwahl ist oft wichtiger als Mikro-Optimierung.

import java.util.*;

final class DuplicateDetector {
    static <T> boolean hasDuplicateSlow(List<T> values) {
        for (int i = 0; i < values.size(); i++)
            for (int j = i + 1; j < values.size(); j++)
                if (Objects.equals(values.get(i), values.get(j))) return true;
        return false;
    }
    static <T> boolean hasDuplicateFast(List<T> values) {
        Set<T> seen = new HashSet<>();
        for (T value : values) if (!seen.add(value)) return true;
        return false;
    }
}

🧩 26. Observability, Logging und Diagnose

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Strukturierter Logger ohne Framework

Key-Value-Logs lassen sich später deutlich besser suchen als Fließtext.

import java.time.*;
import java.util.*;

final class Log {
    static void info(String event, Map<String, ?> fields) { write("INFO", event, fields); }
    static void error(String event, Throwable t, Map<String, ?> fields) {
        Map<String, Object> merged = new LinkedHashMap<>(fields);
        merged.put("error", t.getClass().getSimpleName()); merged.put("message", t.getMessage());
        write("ERROR", event, merged);
    }
    private static void write(String level, String event, Map<String, ?> fields) {
        StringBuilder sb = new StringBuilder().append("ts=").append(Instant.now()).append(" level=").append(level).append(" event=").append(event);
        fields.forEach((k, v) -> sb.append(' ').append(k).append('=').append(String.valueOf(v).replace(" ", "_")));
        System.out.println(sb);
    }
}
Mini-Metrics Registry

Counter und Timer schaffen erste Sichtbarkeit, auch ohne Monitoring-Stack.

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

final class Metrics {
    private final ConcurrentMap<String, LongAdder> counters = new ConcurrentHashMap<>();
    private final ConcurrentMap<String, LongAdder> nanos = new ConcurrentHashMap<>();
    void increment(String name) { counters.computeIfAbsent(name, k -> new LongAdder()).increment(); }
    <T> T time(String name, java.util.concurrent.Callable<T> call) throws Exception {
        long start = System.nanoTime();
        try { return call.call(); }
        finally { nanos.computeIfAbsent(name, k -> new LongAdder()).add(System.nanoTime() - start); }
    }
    Map<String, Long> snapshot() {
        Map<String, Long> out = new TreeMap<>();
        counters.forEach((k, v) -> out.put("counter." + k, v.sum()));
        nanos.forEach((k, v) -> out.put("timer." + k + ".nanos", v.sum()));
        return out;
    }
}

🧩 27. Refactoring und Clean Code

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Primitive Parameter zu Value Object

Ein kleiner Refactoring-Schritt entfernt doppelte Validierung und macht Aufrufe lesbarer.

record EmailAddress(String value) {
    EmailAddress {
        value = value == null ? "" : value.trim().toLowerCase();
        if (!value.matches("^[^@]+@[^@]+\\.[^@]+$")) throw new IllegalArgumentException("invalid email");
    }
}
record InviteUser(EmailAddress email, String role) {}
final class InvitationService {
    void invite(InviteUser command) {
        if (command.role().isBlank()) throw new IllegalArgumentException("role");
        System.out.println("send invite to " + command.email().value());
    }
}
Legacy Adapter isoliert alten Code

Unsichere oder alte APIs werden an einer Stelle eingekapselt.

import java.util.*;

// Alte API: nulls, Strings, unchecked exceptions
interface LegacyCrm { Map<String, String> findCustomer(String id); }
record Customer(String id, String name, String email) {}
final class CustomerLookupAdapter {
    private final LegacyCrm crm;
    CustomerLookupAdapter(LegacyCrm crm) { this.crm = crm; }
    Optional<Customer> find(String id) {
        try {
            Map<String, String> raw = crm.findCustomer(id);
            if (raw == null || raw.isEmpty()) return Optional.empty();
            return Optional.of(new Customer(id, raw.getOrDefault("name", "unknown"), raw.getOrDefault("email", "")));
        } catch (RuntimeException ex) {
            throw new IllegalStateException("CRM unavailable", ex);
        }
    }
}

🧩 28. Event-Sourced Ledger API

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Command Handler mit Event Stream

Der aktuelle Zustand wird aus Events rekonstruiert; Commands erzeugen neue Events.

import java.math.*;
import java.util.*;

sealed interface LedgerEvent permits Opened, Deposited, Withdrawn {}
record Opened(UUID accountId, String owner) implements LedgerEvent {}
record Deposited(BigDecimal amount) implements LedgerEvent {}
record Withdrawn(BigDecimal amount) implements LedgerEvent {}
record LedgerState(boolean opened, BigDecimal balance) {
    static LedgerState empty() { return new LedgerState(false, BigDecimal.ZERO); }
    LedgerState apply(LedgerEvent e) { return switch (e) {
        case Opened o -> new LedgerState(true, balance);
        case Deposited d -> new LedgerState(opened, balance.add(d.amount()));
        case Withdrawn w -> new LedgerState(opened, balance.subtract(w.amount()));
    }; }
}
final class DepositHandler {
    List<LedgerEvent> handle(List<LedgerEvent> history, BigDecimal amount) {
        LedgerState state = history.stream().reduce(LedgerState.empty(), LedgerState::apply, (a,b) -> b);
        if (!state.opened()) throw new IllegalStateException("not opened");
        return List.of(new Deposited(amount));
    }
}
Projection für Read Model

Aus Events entsteht eine schnelle Abfrageansicht, ohne Schreibmodell zu verkomplizieren.

import java.math.*;
import java.util.*;

record AccountView(UUID id, String owner, BigDecimal balance, long version) {}
final class AccountProjection {
    private final Map<UUID, AccountView> views = new HashMap<>();
    void project(UUID streamId, long version, LedgerEvent event) {
        AccountView current = views.getOrDefault(streamId, new AccountView(streamId, "?", BigDecimal.ZERO, 0));
        AccountView next = switch (event) {
            case Opened e -> new AccountView(streamId, e.owner(), BigDecimal.ZERO, version);
            case Deposited e -> new AccountView(streamId, current.owner(), current.balance().add(e.amount()), version);
            case Withdrawn e -> new AccountView(streamId, current.owner(), current.balance().subtract(e.amount()), version);
        };
        views.put(streamId, next);
    }
    Optional<AccountView> find(UUID id) { return Optional.ofNullable(views.get(id)); }
}

🧩 29. 24-Wochen-Masterplan als Code

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Lernplan-Generator

Der Lernpfad wird als Datenmodell greifbar und kann später exportiert oder gefiltert werden.

import java.util.*;

record Week(int number, String theme, List<String> deliverables) {}
final class LearningPlan {
    static List<Week> javaMasterPlan() {
        return List.of(
            new Week(1, "Toolchain", List.of("JDK installieren", "CLI build script")),
            new Week(6, "OOP und Records", List.of("Value Objects", "Aggregate kata")),
            new Week(14, "Concurrency", List.of("Virtual thread crawler", "Bulkhead")),
            new Week(21, "JDBC", List.of("Repository", "Outbox")),
            new Week(24, "Portfolio", List.of("README", "Tests", "Demo script"))
        );
    }
    static void print(List<Week> weeks) { weeks.forEach(w -> System.out.println("W" + w.number() + " " + w.theme() + " -> " + w.deliverables())); }
}
Fortschritts-Tracker

Mastery wird messbar: erledigte Deliverables, offene Lücken und Fokus der nächsten Woche.

import java.util.*;

record Progress(String item, boolean done) {}
final class ProgressTracker {
    private final Map<Integer, List<Progress>> byWeek = new TreeMap<>();
    void mark(int week, String item, boolean done) {
        byWeek.computeIfAbsent(week, k -> new ArrayList<>()).add(new Progress(item, done));
    }
    double completion() {
        long total = byWeek.values().stream().flatMap(List::stream).count();
        long done = byWeek.values().stream().flatMap(List::stream).filter(Progress::done).count();
        return total == 0 ? 0 : (double) done / total;
    }
    List<String> openItems() { return byWeek.values().stream().flatMap(List::stream).filter(p -> !p.done()).map(Progress::item).toList(); }
}

🧩 30. Checklisten, Interviewfragen und Portfolio

mehrere BeispieleMaster-Niveaudirekt kopierbaroffline highlighted
Top-N Interview-Kata

Zeigt Collections, Comparator, Immutability und API-Design in einer kleinen Aufgabe.

import java.util.*;

final class TopN<T> {
    private final int n;
    private final Comparator<T> comparator;
    private final PriorityQueue<T> heap;
    TopN(int n, Comparator<T> comparator) {
        this.n = n; this.comparator = comparator; this.heap = new PriorityQueue<>(comparator);
    }
    void add(T value) {
        if (heap.size() < n) heap.add(value);
        else if (comparator.compare(value, heap.peek()) > 0) { heap.poll(); heap.add(value); }
    }
    List<T> snapshotDescending() { return heap.stream().sorted(comparator.reversed()).toList(); }
}
Portfolio Readiness Scorer

Ein kleines Bewertungsmodell macht sichtbar, ob ein Projekt präsentationsreif ist.

import java.util.*;

record PortfolioCheck(String name, int points, boolean passed) {}
final class PortfolioScorer {
    int score(List<PortfolioCheck> checks) { return checks.stream().filter(PortfolioCheck::passed).mapToInt(PortfolioCheck::points).sum(); }
    List<String> missingCritical(List<PortfolioCheck> checks) {
        return checks.stream().filter(c -> !c.passed() && c.points() >= 20).map(PortfolioCheck::name).toList();
    }
    boolean ready(List<PortfolioCheck> checks) { return score(checks) >= 80 && missingCritical(checks).isEmpty(); }
}
Modul E

🛒 CommerceFlow Master-Projekt

Durchgehendes Projekt mit Domain Core, Application Layer, HTTP/JSON, Outbox, Worker, Tests und Runbook.

7Themen in diesem Modul
13Codeblöcke
1SVG-Grafiken

Durchgehendes Master-Projekt: CommerceFlow – Architekturüberblick

Modul E · CommerceFlow Master-Projekt · Alt-Referenz: K58

Projektziel

CommerceFlow ist ein bewusst frameworkarmes Java-Masterprojekt, das die Kapitel verbindet: Domain-Modell, Use Cases, REST-Adapter, I/O-Outbox, Concurrency-Worker, Build-Info und Tests. Du kannst es später leicht auf Spring Boot, Quarkus oder Micronaut übertragen, aber hier lernst du zuerst die Kernprinzipien ohne Framework-Magie.

CommerceFlow Gesamtarchitektur HTTPAdapterApplicationUse CasesDomainRulesPortsInterfacesAdaptersFile/Memory
Projektstruktur
commerceflow/
├─ src/main/java/com/seb4u/demo/commerce/domain/…
├─ src/main/java/com/seb4u/demo/commerce/application/…
├─ src/main/java/com/seb4u/demo/commerce/adapter/http/…
├─ src/main/java/com/seb4u/demo/commerce/adapter/file/…
├─ src/main/java/com/seb4u/demo/commerce/bootstrap/…
└─ src/test/java/com/seb4u/demo/commerce/…
BausteinLernzielKapitelbezug
Domain CoreInvarianten ohne Framework ausdrückenRecords, sealed Types, Exceptions
Application LayerUse Cases, Ports und TransaktionsgrenzenArchitektur, Generics, Fehlerverträge
HTTP AdapterRequests validieren und Antworten standardisierenI/O, Exceptions, JSON
OutboxDatei-I/O atomar und idempotent nutzenI/O/NIO Deep Dive
WorkerNebenläufigkeit kontrollierenConcurrency + CompletableFuture

Master-Projekt: Domain Core – Order, Money, Events und Invarianten

Modul E · CommerceFlow Master-Projekt · Alt-Referenz: K59

Der Domain Core enthält keine HTTP-Klassen, keine SQL-Klassen und keine Framework-Annotationen. Er modelliert Sprache und Regeln: Bestellung, Positionen, Geld, Statuswechsel und Domain Events.

Code 59.1 – Domain Model mit Records, sealed Events und Statusmaschine
package com.seb4u.demo.commerce.domain;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.util.*;

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 Status status = Status.DRAFT;
    private Instant submittedAt;
    private String paymentReference;

    private Order(OrderId id, CustomerId customerId) {
        this.id = Objects.requireNonNull(id);
        this.customerId = Objects.requireNonNull(customerId);
        events.add(new OrderCreated(id, customerId, Instant.now()));
    }

    public static Order draft(OrderId id, CustomerId customerId) { return new Order(id, customerId); }

    public void addLine(Sku sku, int quantity, Money unitPrice) {
        require(status == Status.DRAFT, "order_not_editable");
        require(quantity > 0, "quantity_must_be_positive");
        lines.add(new OrderLine(sku, quantity, unitPrice));
    }

    public void submit(Instant now) {
        require(status == Status.DRAFT, "only_draft_can_be_submitted");
        require(!lines.isEmpty(), "empty_order");
        status = Status.SUBMITTED;
        submittedAt = now;
        events.add(new OrderSubmitted(id, now, total()));
    }

    public void markPaid(String reference) {
        require(status == Status.SUBMITTED, "only_submitted_can_be_paid");
        paymentReference = requireText(reference, "payment_reference_required");
        status = Status.PAID;
        events.add(new OrderPaid(id, paymentReference, Instant.now()));
    }

    public Money total() {
        return lines.stream().map(OrderLine::subtotal).reduce(Money.zero("EUR"), Money::add);
    }

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

    private static void require(boolean ok, String code) { if (!ok) throw new DomainException(code); }
    private static String requireText(String value, String code) {
        if (value == null || value.isBlank()) throw new DomainException(code);
        return value;
    }

    public enum Status { DRAFT, SUBMITTED, PAID, CANCELLED }
    public record OrderId(UUID value) { public static OrderId newId() { return new OrderId(UUID.randomUUID()); } }
    public record CustomerId(String value) { public CustomerId { requireText(value, "customer_required"); } }
    public record Sku(String value) { public Sku { requireText(value, "sku_required"); } }

    public record OrderLine(Sku sku, int quantity, Money unitPrice) {
        public Money subtotal() { return unitPrice.multiply(quantity); }
    }

    public record Money(BigDecimal amount, Currency currency) {
        public Money {
            Objects.requireNonNull(amount); Objects.requireNonNull(currency);
            amount = amount.setScale(2, RoundingMode.HALF_UP);
        }
        public static Money zero(String currency) { return new Money(BigDecimal.ZERO, Currency.getInstance(currency)); }
        public Money add(Money other) {
            if (!currency.equals(other.currency)) throw new DomainException("currency_mismatch");
            return new Money(amount.add(other.amount), currency);
        }
        public Money multiply(int factor) { return new Money(amount.multiply(BigDecimal.valueOf(factor)), currency); }
    }

    public sealed interface DomainEvent permits OrderCreated, OrderSubmitted, OrderPaid {
        OrderId orderId(); Instant occurredAt();
    }
    public record OrderCreated(OrderId orderId, CustomerId customerId, Instant occurredAt) implements DomainEvent {}
    public record OrderSubmitted(OrderId orderId, Instant occurredAt, Money total) implements DomainEvent {}
    public record OrderPaid(OrderId orderId, String reference, Instant occurredAt) implements DomainEvent {}
    public static final class DomainException extends RuntimeException { public DomainException(String code) { super(code); } }
}
Code 59.2 – Fachliche Spezifikationen für Rabatt- und Risikoentscheidungen
package com.seb4u.demo.commerce.domain;

import java.math.BigDecimal;
import java.util.function.Predicate;

public interface Specification<T> extends Predicate<T> {
    default Specification<T> and(Specification<T> other) { return value -> test(value) && other.test(value); }
    default Specification<T> or(Specification<T> other) { return value -> test(value) || other.test(value); }
    default Specification<T> not() { return value -> !test(value); }

    static Specification<OrderSnapshot> minimumTotal(BigDecimal value) {
        return order -> order.total().amount().compareTo(value) >= 0;
    }

    static Specification<OrderSnapshot> trustedCustomer() {
        return OrderSnapshot::trustedCustomer;
    }

    record OrderSnapshot(Order.OrderId id, Order.Money total, boolean trustedCustomer, int lineCount) {}
}

Master-Projekt: Application Layer – Use Cases, Ports und Fehlerverträge

Modul E · CommerceFlow Master-Projekt · Alt-Referenz: K60

Die Application-Schicht orchestriert: Sie lädt Daten über Ports, ruft Domain-Verhalten auf, speichert Ergebnisse und veröffentlicht Events. Sie enthält technische Transaktionsgrenzen, aber keine Framework-Abhängigkeit.

Code 60.1 – PlaceOrderUseCase mit Ports, Validation und Result Contract
package com.seb4u.demo.commerce.application;

import com.seb4u.demo.commerce.domain.Order;
import java.math.BigDecimal;
import java.time.Clock;
import java.util.*;

public final class PlaceOrderUseCase {
    private final CatalogPort catalog;
    private final OrderRepository orders;
    private final PaymentPort payments;
    private final EventPublisher events;
    private final Clock clock;

    public PlaceOrderUseCase(CatalogPort catalog, OrderRepository orders, PaymentPort payments,
                             EventPublisher events, Clock clock) {
        this.catalog = catalog; this.orders = orders; this.payments = payments; this.events = events; this.clock = clock;
    }

    public Result<Order.OrderId> handle(Command command) {
        List<String> errors = validate(command);
        if (!errors.isEmpty()) return Result.validation(errors);
        try {
            Order order = Order.draft(Order.OrderId.newId(), new Order.CustomerId(command.customerId()));
            for (Line line : command.lines()) {
                Product product = catalog.require(line.sku());
                order.addLine(new Order.Sku(product.sku()), line.quantity(), product.price());
            }
            order.submit(clock.instant());
            PaymentReceipt receipt = payments.authorize(order.id(), order.total());
            order.markPaid(receipt.reference());
            orders.save(order);
            events.publish(order.pullEvents());
            return Result.ok(order.id());
        } catch (Order.DomainException ex) {
            return Result.business(ex.getMessage());
        } catch (Exception ex) {
            return Result.technical("place_order_failed", ex);
        }
    }

    private static List<String> validate(Command c) {
        List<String> e = new ArrayList<>();
        if (c.customerId() == null || c.customerId().isBlank()) e.add("customer_required");
        if (c.lines() == null || c.lines().isEmpty()) e.add("lines_required");
        return e;
    }

    public record Command(String customerId, List<Line> lines) {}
    public record Line(String sku, int quantity) {}
    public record Product(String sku, Order.Money price) {}
    public record PaymentReceipt(String reference) {}

    public interface CatalogPort { Product require(String sku); }
    public interface OrderRepository { void save(Order order); }
    public interface PaymentPort { PaymentReceipt authorize(Order.OrderId id, Order.Money total); }
    public interface EventPublisher { void publish(List<Order.DomainEvent> events); }

    public sealed interface Result<T> permits Result.Ok, Result.Validation, Result.Business, Result.Technical {
        record Ok<T>(T value) implements Result<T> {}
        record Validation<T>(List<String> errors) implements Result<T> {}
        record Business<T>(String code) implements Result<T> {}
        record Technical<T>(String code, Throwable cause) implements Result<T> {}
        static <T> Result<T> ok(T value) { return new Ok<>(value); }
        static <T> Result<T> validation(List<String> errors) { return new Validation<>(List.copyOf(errors)); }
        static <T> Result<T> business(String code) { return new Business<>(code); }
        static <T> Result<T> technical(String code, Throwable cause) { return new Technical<>(code, cause); }
    }
}
Code 60.2 – Transaction Script Adapter ohne Framework-Magie
package com.seb4u.demo.commerce.application;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.function.Function;

public final class TransactionTemplate {
    private final ConnectionProvider provider;

    public TransactionTemplate(ConnectionProvider provider) { this.provider = provider; }

    public <T> T tx(Function<Connection, T> work) {
        try (Connection c = provider.get()) {
            boolean old = c.getAutoCommit();
            c.setAutoCommit(false);
            try {
                T result = work.apply(c);
                c.commit();
                return result;
            } catch (RuntimeException e) {
                c.rollback();
                throw e;
            } finally {
                c.setAutoCommit(old);
            }
        } catch (SQLException e) {
            throw new IllegalStateException("transaction_failed", e);
        }
    }

    public interface ConnectionProvider { Connection get() throws SQLException; }
}

Master-Projekt: HTTP/JSON Adapter – klare Systemgrenze

Modul E · CommerceFlow Master-Projekt · Alt-Referenz: K61

Der HTTP-Adapter wandelt untrusted Input in Commands und Application-Resultate in stabile HTTP-Antworten. Der Adapter besitzt Parsing-Details; die Domain bleibt frei von HTTP.

Code 61.1 – Minimaler HTTP Server mit Routing, Statuscodes und Error Contract
package com.seb4u.demo.commerce.adapter.http;

import com.seb4u.demo.commerce.application.PlaceOrderUseCase;
import com.sun.net.httpserver.*;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;

public final class CommerceHttpServer implements AutoCloseable {
    private final HttpServer server;
    private final PlaceOrderUseCase placeOrder;

    public CommerceHttpServer(int port, PlaceOrderUseCase placeOrder) throws IOException {
        this.placeOrder = placeOrder;
        this.server = HttpServer.create(new InetSocketAddress(port), 0);
        this.server.createContext("/orders", this::orders);
        this.server.createContext("/health", this::health);
        this.server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
    }

    public void start() { server.start(); }
    @Override public void close() { server.stop(0); }

    private void orders(HttpExchange ex) throws IOException {
        if (!"POST".equals(ex.getRequestMethod())) { respond(ex, 405, "{\"error\":\"method_not_allowed\"}"); return; }
        String body = new String(ex.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
        PlaceOrderUseCase.Command command = JsonOrderParser.parse(body);
        PlaceOrderUseCase.Result<?> result = placeOrder.handle(command);
        switch (result) {
            case PlaceOrderUseCase.Result.Ok<?> ok -> respond(ex, 201, "{\"orderId\":\"" + ok.value() + "\"}");
            case PlaceOrderUseCase.Result.Validation<?> v -> respond(ex, 400, "{\"errors\":" + JsonOrderParser.array(v.errors()) + "}");
            case PlaceOrderUseCase.Result.Business<?> b -> respond(ex, 409, "{\"error\":\"" + b.code() + "\"}");
            case PlaceOrderUseCase.Result.Technical<?> t -> respond(ex, 500, "{\"error\":\"" + t.code() + "\"}");
        }
    }

    private void health(HttpExchange ex) throws IOException { respond(ex, 200, "{\"status\":\"UP\"}"); }

    private static void respond(HttpExchange ex, int status, String json) throws IOException {
        byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
        ex.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
        ex.sendResponseHeaders(status, bytes.length);
        ex.getResponseBody().write(bytes);
        ex.close();
    }
}
Code 61.2 – Kleiner JSON Parser für bewusst kontrolliertes Demo-Format
package com.seb4u.demo.commerce.adapter.http;

import com.seb4u.demo.commerce.application.PlaceOrderUseCase;
import java.util.*;
import java.util.regex.*;

public final class JsonOrderParser {
    private static final Pattern CUSTOMER = Pattern.compile("\\\"customerId\\\"\\s*:\\s*\\\"([^\\\"]+)\\\"");
    private static final Pattern LINE = Pattern.compile("\\{\\s*\\\"sku\\\"\\s*:\\s*\\\"([^\\\"]+)\\\"\\s*,\\s*\\\"quantity\\\"\\s*:\\s*(\\d+)\\s*}");

    public static PlaceOrderUseCase.Command parse(String json) {
        String customer = match(CUSTOMER, json).orElse("");
        List<PlaceOrderUseCase.Line> lines = new ArrayList<>();
        Matcher m = LINE.matcher(json);
        while (m.find()) lines.add(new PlaceOrderUseCase.Line(m.group(1), Integer.parseInt(m.group(2))));
        return new PlaceOrderUseCase.Command(customer, lines);
    }

    public static String array(List<String> values) {
        return values.stream().map(v -> "\"" + escape(v) + "\"").toList().toString();
    }

    private static Optional<String> match(Pattern p, String text) {
        Matcher m = p.matcher(text);
        return m.find() ? Optional.of(m.group(1)) : Optional.empty();
    }

    private static String escape(String s) { return s.replace("\\", "\\\\").replace("\"", "\\\""); }
}

Master-Projekt: I/O Adapter – Outbox, Snapshots und idempotente Dateien

Modul E · CommerceFlow Master-Projekt · Alt-Referenz: K62

Die Outbox koppelt Domain Events vom späteren Versand. Datei-I/O wird atomar: erst temporär schreiben, dann verschieben. Dadurch entstehen keine halbfertigen Event-Dateien.

Code 62.1 – FileOutbox mit atomarem Write und Event-Envelope
package com.seb4u.demo.commerce.adapter.file;

import com.seb4u.demo.commerce.application.PlaceOrderUseCase;
import com.seb4u.demo.commerce.domain.Order;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.time.Instant;
import java.util.*;

public final class FileOutbox implements PlaceOrderUseCase.EventPublisher {
    private final Path dir;

    public FileOutbox(Path dir) throws IOException {
        this.dir = Files.createDirectories(dir.toAbsolutePath().normalize());
    }

    @Override public void publish(List<Order.DomainEvent> events) {
        for (Order.DomainEvent event : events) write(event);
    }

    private void write(Order.DomainEvent event) {
        try {
            String id = Instant.now().toEpochMilli() + "-" + UUID.randomUUID();
            Path tmp = dir.resolve(id + ".json.tmp");
            Path target = dir.resolve(id + ".json");
            Files.writeString(tmp, envelope(event), StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW);
            Files.move(tmp, target, StandardCopyOption.ATOMIC_MOVE);
        } catch (IOException e) {
            throw new IllegalStateException("outbox_write_failed", e);
        }
    }

    private static String envelope(Order.DomainEvent e) {
        return "{\"type\":\"" + e.getClass().getSimpleName() + "\","
             + "\"orderId\":\"" + e.orderId().value() + "\","
             + "\"occurredAt\":\"" + e.occurredAt() + "\"}";
    }
}
Code 62.2 – OutboxRelay mit idempotentem Archiv und Quarantäne
package com.seb4u.demo.commerce.adapter.file;

import java.io.IOException;
import java.nio.file.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;

public final class OutboxRelay implements Runnable {
    private final Path outbox, sent, quarantine;
    private final EventSender sender;
    private final AtomicBoolean running = new AtomicBoolean(true);

    public OutboxRelay(Path outbox, EventSender sender) throws IOException {
        this.outbox = Files.createDirectories(outbox);
        this.sent = Files.createDirectories(outbox.resolveSibling("sent"));
        this.quarantine = Files.createDirectories(outbox.resolveSibling("quarantine"));
        this.sender = sender;
    }

    @Override public void run() {
        while (running.get()) {
            try { drainOnce(); Thread.sleep(500); }
            catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
            catch (Exception e) { System.err.println("relay error: " + e.getMessage()); }
        }
    }

    public void stop() { running.set(false); }

    public void drainOnce() throws IOException {
        try (Stream<Path> files = Files.list(outbox)) {
            for (Path file : files.filter(p -> p.toString().endsWith(".json")).sorted().toList()) {
                try {
                    sender.send(Files.readString(file));
                    Files.move(file, sent.resolve(file.getFileName()), StandardCopyOption.REPLACE_EXISTING);
                } catch (Exception ex) {
                    Files.move(file, quarantine.resolve(file.getFileName()), StandardCopyOption.REPLACE_EXISTING);
                }
            }
        }
    }

    public interface EventSender { void send(String json) throws Exception; }
}

Master-Projekt: Concurrency Worker – Imports, Outbox und kontrollierte Parallelität

Modul E · CommerceFlow Master-Projekt · Alt-Referenz: K63

CommerceFlow nutzt Virtual Threads für blockierende Arbeit, aber mit Limits. Worker sind stoppbar, liefern Fehler sichtbar zurück und verhindern unendliche Queues.

Code 63.1 – ApplicationRuntime mit Virtual Threads, Shutdown Hook und Worker Lifecycle
package com.seb4u.demo.commerce.bootstrap;

import com.seb4u.demo.commerce.adapter.file.OutboxRelay;
import java.util.*;
import java.util.concurrent.*;

public final class ApplicationRuntime implements AutoCloseable {
    private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
    private final List<AutoCloseable> closeables = new CopyOnWriteArrayList<>();
    private final List<Future<?>> tasks = new CopyOnWriteArrayList<>();

    public void startWorker(String name, Runnable worker) {
        Future<?> f = executor.submit(() -> {
            Thread.currentThread().setName(name);
            worker.run();
        });
        tasks.add(f);
    }

    public void register(AutoCloseable c) { closeables.add(c); }

    @Override public void close() {
        for (AutoCloseable c : closeables.reversed()) {
            try { c.close(); } catch (Exception e) { System.err.println("close failed: " + e.getMessage()); }
        }
        for (Future<?> task : tasks) task.cancel(true);
        executor.close();
    }
}
Code 63.2 – Rate-limited Import Worker mit Retry und Dead-letter
package com.seb4u.demo.commerce.adapter.file;

import java.nio.file.*;
import java.time.Duration;
import java.util.concurrent.Semaphore;

public final class ImportWorker {
    private final Semaphore rate = new Semaphore(10);
    private final Path deadLetter;

    public ImportWorker(Path deadLetter) throws Exception {
        this.deadLetter = Files.createDirectories(deadLetter);
    }

    public void importFile(Path file) throws Exception {
        rate.acquire();
        try {
            retry(3, Duration.ofMillis(150), () -> {
                // parse + validate + call use case
                if (Files.size(file) == 0) throw new IllegalArgumentException("empty file");
            });
        } catch (Exception ex) {
            Files.move(file, deadLetter.resolve(file.getFileName()), StandardCopyOption.REPLACE_EXISTING);
        } finally {
            rate.release();
        }
    }

    private static void retry(int attempts, Duration backoff, CheckedRunnable action) throws Exception {
        Exception last = null;
        for (int i = 1; i <= attempts; i++) {
            try { action.run(); return; }
            catch (Exception e) { last = e; Thread.sleep(backoff.multipliedBy(i).toMillis()); }
        }
        throw last;
    }

    interface CheckedRunnable { void run() throws Exception; }
}

Master-Projekt: Tests, Build und Runbook

Modul E · CommerceFlow Master-Projekt · Alt-Referenz: K64

Das Projekt wird erst wertvoll, wenn es wiederholbar ausführbar und testbar ist. Dieses Kapitel gibt dir eine minimale Teststrategie, Build-Kommandos und Betriebschecks.

Lokaler Start

./mvnw test package
java -jar target/commerceflow.jar

Smoke Test

POST /orders, danach Outbox-Datei prüfen.

Failure Test

Ungültige Bestellung senden und stabilen Error Contract prüfen.

Code 64.1 – Frameworkloser Domain-Test mit Arrange/Act/Assert
package com.seb4u.demo.commerce;

import com.seb4u.demo.commerce.domain.Order;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Currency;

public final class OrderDomainTest {
    public static void main(String[] args) {
        Order order = Order.draft(Order.OrderId.newId(), new Order.CustomerId("C-100"));
        order.addLine(new Order.Sku("BOOK-1"), 2, new Order.Money(new BigDecimal("19.90"), Currency.getInstance("EUR")));
        order.submit(Instant.parse("2026-01-01T10:00:00Z"));
        assertEquals("39.80", order.total().amount().toPlainString());
        assertThrows(() -> order.addLine(new Order.Sku("LATE"), 1, Order.Money.zero("EUR")));
        System.out.println("OrderDomainTest passed");
    }

    static void assertEquals(Object expected, Object actual) {
        if (!expected.equals(actual)) throw new AssertionError("expected=" + expected + " actual=" + actual);
    }

    static void assertThrows(Runnable r) {
        try { r.run(); throw new AssertionError("exception expected"); }
        catch (RuntimeException expected) { /* ok */ }
    }
}
Code 64.2 – Integrationstest-Idee: HTTP rein, Outbox-Datei raus
package com.seb4u.demo.commerce;

import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.time.Duration;

public final class CommerceSmokeTest {
    public static void main(String[] args) throws Exception {
        Path outbox = Path.of("build/test-outbox");
        Files.createDirectories(outbox);

        HttpClient client = HttpClient.newHttpClient();
        String json = """
            {"customerId":"C-100","lines":[{"sku":"BOOK-1","quantity":2}]}
            """;
        HttpRequest req = HttpRequest.newBuilder(URI.create("http://localhost:8080/orders"))
                .timeout(Duration.ofSeconds(2))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(json))
                .build();

        HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
        if (res.statusCode() != 201) throw new AssertionError(res.statusCode() + " " + res.body());
        long eventFiles = Files.list(outbox).filter(p -> p.toString().endsWith(".json")).count();
        if (eventFiles == 0) throw new AssertionError("No outbox events written");
    }
}
Code 64.3 – Runbook-Healthcheck mit BuildInfo und Speicherstatus
package com.seb4u.demo.commerce.bootstrap;

import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;

public final class HealthReport {
    private final BuildInfo build;
    private final MemoryMXBean memory = ManagementFactory.getMemoryMXBean();

    public HealthReport(BuildInfo build) { this.build = build; }

    public String json() {
        long heap = memory.getHeapMemoryUsage().getUsed();
        return "{"
                + "\"status\":\"UP\","
                + "\"version\":\"" + build.version() + "\","
                + "\"commit\":\"" + build.commit() + "\","
                + "\"heapUsed\":" + heap
                + "}";
    }
}
Portfolio-Aufgabe: Baue CommerceFlow lauffähig nach, ersetze den InMemory-Adapter durch JDBC und ergänze JUnit-Tests.
Interview-Aufgabe: Erkläre, warum die Domain keine HTTP-, SQL- oder JSON-Klassen importiert.
Modul F

🏢 Enterprise Java: Spring, Persistence, Testing, Security & Production

Spring Boot, Datenbank/Persistence, Testing, Security, Production Readiness, Enterprise Upgrade und Release Runbook.

8Themen in diesem Modul
23Codeblöcke
8SVG-Grafiken
🏢

Enterprise Edition Überblick: Von Java Core zu Production Java

Modul F · Enterprise Java: Spring, Persistence, Testing, Security & Production · Alt-Referenz: K65

Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.

Was diese Edition ergänzt

Diese Erweiterung macht aus dem bisherigen Java-Master-Handbuch eine Enterprise-Lernstrecke: Spring Boot, Persistence, Tests, Security, Deployments und Production Readiness werden als zusammenhängendes System erklärt.

Spring Boot 4.x kompatibelJDK 25/26 ReadyOffline-HandbuchJob-nahes Projektdenken
Enterprise-Lernpyramide Enterprise-Lernpyramide Core Java & Sprache Architektur & Domain Spring Boot & Adapter Persistence & Transaktionen Security, Tests, Observability Production, Docker, CI/CD
StufeWas du können musstTypische FehlerMaster-Regel
FrameworkAuto-Configuration verstehen, nicht nur Annotationen kopierenBusinesslogik im ControllerController übersetzen HTTP, Services entscheiden fachlich
DatenbankTransaktionen, Locking, Migrationen, PoolingLazy Loading in REST-AntwortenDTOs an Systemgrenzen, Domain im Kern
TestsTestpyramide, Testcontainers, Contract-TestsNur Mock-Tests ohne echte IntegrationWichtigste Risiken mit echten Abhängigkeiten testen
ProductionLogs, Metrics, Health, Rollback, SecretsDeployment ohne RunbookJeder Fehler braucht Diagnosepfad und sicheren Fallback
Lernziel: Du kannst eine Java-Anwendung von Domain-Code bis Deployment erklären.
Praxisziel: Du kannst CommerceFlow zu einer Spring-Boot-basierten Enterprise-App erweitern.
Interviewziel: Du kannst begründen, wann du JPA, JDBC, Events, DTOs, Transactions und Security-Filter einsetzt.
🌱

Spring Boot Professional Deep Dive

Modul F · Enterprise Java: Spring, Persistence, Testing, Security & Production · Alt-Referenz: K66

Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.

Spring Boot Schichtenmodell Spring Boot Schichtenmodell HTTP Controller Application Service / Use Case Domain Model Repository Port Adapter: JDBC/JPA/HTTP/Queue

Spring Boot ist nicht „Magie“, sondern ein Satz von Konventionen: Auto-Configuration, Starter, Dependency Injection, Properties Binding, embedded Server, Observability und Production-Endpunkte. Professionelle Entwickler verstehen, welche Schicht welche Verantwortung trägt.

Master-Regel: Annotationen gehören an Infrastrukturgrenzen. Geschäftsregeln bleiben in normalen Java-Klassen testbar.

Controller

Validiert Protokollform, ruft Use Case, übersetzt Ergebnis in HTTP.

Service

Koordiniert fachliche Operationen, Transaktionen und Ports.

Domain

Enthält Invarianten, Value Objects, Events, Zustandsübergänge.

Adapter

Implementiert Datenbank, Messaging, Filesystem, externe APIs.

Code 1: Saubere Spring-Boot-Struktur mit Constructor Injection

package com.seb4u.demo.spring.commerceflow.orders.api;

import com.seb4u.demo.spring.commerceflow.orders.app.PlaceOrderUseCase;
import com.seb4u.demo.spring.commerceflow.orders.app.PlaceOrderCommand;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;
import java.util.List;

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

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

    @PostMapping
    ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest request) {
        var command = new PlaceOrderCommand(
                request.customerId(),
                request.lines().stream().map(l -> new PlaceOrderCommand.Line(l.sku(), l.quantity())).toList()
        );

        return placeOrder.handle(command).fold(
                error -> ResponseEntity.badRequest().body(OrderResponse.failed(error.code(), error.message())),
                order -> ResponseEntity
                        .created(URI.create("/api/orders/" + order.id()))
                        .body(OrderResponse.created(order.id(), order.status().name()))
        );
    }

    record CreateOrderRequest(@NotBlank String customerId, @Valid List<Line> lines) {
        record Line(@NotBlank String sku, @Positive int quantity) {}
    }

    record OrderResponse(String id, String status, String errorCode, String message) {
        static OrderResponse created(String id, String status) { return new OrderResponse(id, status, null, null); }
        static OrderResponse failed(String code, String message) { return new OrderResponse(null, "REJECTED", code, message); }
    }
}

Code 2: Configuration Properties statt verstreuter Konstanten

package com.seb4u.demo.spring.commerceflow.config;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

import java.time.Duration;

@Validated
@ConfigurationProperties(prefix = "commerceflow.outbox")
public record OutboxProperties(
        @NotBlank String directory,
        @Min(1) int workerThreads,
        Duration pollInterval,
        Retry retry
) {
    public OutboxProperties {
        if (pollInterval == null) pollInterval = Duration.ofSeconds(2);
        if (retry == null) retry = new Retry(5, Duration.ofMillis(200));
    }

    public record Retry(@Min(1) int maxAttempts, Duration initialDelay) {}
}

Code 3: Globaler Fehlervertrag mit Problem Details

package com.seb4u.demo.spring.commerceflow.errors;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.net.URI;
import java.time.Instant;
import java.util.Map;
import java.util.stream.Collectors;

@RestControllerAdvice
final class ApiExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    ProblemDetail validation(MethodArgumentNotValidException ex, HttpServletRequest request) {
        var errors = ex.getBindingResult().getFieldErrors().stream()
                .collect(Collectors.groupingBy(
                        e -> e.getField(),
                        Collectors.mapping(e -> e.getDefaultMessage(), Collectors.toList())
                ));
        var pd = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
        pd.setType(URI.create("urn:problem:validation"));
        pd.setTitle("Request validation failed");
        pd.setDetail("At least one field violates the API contract.");
        pd.setProperty("path", request.getRequestURI());
        pd.setProperty("timestamp", Instant.now().toString());
        pd.setProperty("errors", errors);
        return pd;
    }

    @ExceptionHandler(IllegalStateException.class)
    ProblemDetail conflict(IllegalStateException ex) {
        var pd = ProblemDetail.forStatus(HttpStatus.CONFLICT);
        pd.setType(URI.create("urn:problem:conflict"));
        pd.setTitle("State conflict");
        pd.setDetail(ex.getMessage());
        return pd;
    }
}
🗄️

Datenbank & Persistence Masterclass

Modul F · Enterprise Java: Spring, Persistence, Testing, Security & Production · Alt-Referenz: K67

Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.

Persistence-Fluss Persistence-Fluss DTO Use Case Transaction Repository Port JDBC/JPA Adapter DB

Enterprise-Java scheitert häufig nicht an SQL, sondern an unklaren Grenzen: Entity wird direkt als API-Modell genutzt, Transaktionen sind zu groß, Lazy Loading passiert im Serializer, Migrationen fehlen, und Locking wird erst in Production entdeckt.

ThemaJDBCJPA/HibernateMaster-Hinweis
KontrolleSehr hochMittelJDBC für kritische Queries und kleine Domänen
ProduktivitätMittelHochJPA für CRUD-nahe Aggregate
Performance-FallenManuelles MappingN+1, Lazy LoadingImmer SQL beobachten
TransaktionenExplizitDeklarativUse-Case-Grenze als Transaktionsgrenze

Code 1: Transactional Use Case mit Repository Port

package com.seb4u.demo.spring.commerceflow.orders.app;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ConfirmOrderService {
    private final OrderRepository orders;
    private final InventoryPort inventory;
    private final OutboxPort outbox;

    public ConfirmOrderService(OrderRepository orders, InventoryPort inventory, OutboxPort outbox) {
        this.orders = orders;
        this.inventory = inventory;
        this.outbox = outbox;
    }

    @Transactional
    public ConfirmOrderResult confirm(String orderId) {
        var order = orders.findByIdForUpdate(orderId)
                .orElseThrow(() -> new IllegalArgumentException("Order not found: " + orderId));

        if (order.isConfirmed()) return ConfirmOrderResult.alreadyConfirmed(order.id());

        var reservation = inventory.reserve(order.requiredItems());
        if (!reservation.accepted()) {
            order.reject("OUT_OF_STOCK");
            orders.save(order);
            outbox.append(order.pullDomainEvents());
            return ConfirmOrderResult.rejected(order.id(), reservation.reason());
        }

        order.confirm(reservation.reservationId());
        orders.save(order);
        outbox.append(order.pullDomainEvents());
        return ConfirmOrderResult.confirmed(order.id());
    }
}

Code 2: JDBC RowMapper + Optimistic Locking

package com.seb4u.demo.spring.commerceflow.orders.adapter.jdbc;

import com.seb4u.demo.spring.commerceflow.orders.domain.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;

@Repository
final class JdbcOrderRepository implements OrderRepository {
    private final JdbcTemplate jdbc;

    JdbcOrderRepository(JdbcTemplate jdbc) { this.jdbc = jdbc; }

    public Optional<Order> findByIdForUpdate(String id) {
        return jdbc.query("""
                select id, customer_id, status, version
                from orders
                where id = ?
                for update
                """, this::mapOrder, id).stream().findFirst();
    }

    public void save(Order order) {
        int updated = jdbc.update("""
                update orders
                set status = ?, version = version + 1, updated_at = current_timestamp
                where id = ? and version = ?
                """, order.status().name(), order.id(), order.version());

        if (updated != 1) throw new OptimisticLockException("Order changed concurrently: " + order.id());
    }

    private Order mapOrder(ResultSet rs, int row) throws SQLException {
        return Order.rehydrate(
                rs.getString("id"),
                rs.getString("customer_id"),
                Order.Status.valueOf(rs.getString("status")),
                rs.getLong("version")
        );
    }
}

Code 3: Flyway-Migration als fachlicher Vertrag

-- V4__orders_outbox_and_idempotency.sql
create table orders (
  id varchar(36) primary key,
  customer_id varchar(64) not null,
  status varchar(32) not null,
  version bigint not null default 0,
  created_at timestamp not null default current_timestamp,
  updated_at timestamp not null default current_timestamp
);

create table idempotency_keys (
  key varchar(128) primary key,
  request_hash varchar(128) not null,
  response_json text not null,
  created_at timestamp not null default current_timestamp
);

create table outbox_events (
  id varchar(36) primary key,
  aggregate_id varchar(36) not null,
  event_type varchar(128) not null,
  payload text not null,
  attempts int not null default 0,
  available_at timestamp not null default current_timestamp,
  processed_at timestamp null
);

create index ix_outbox_available on outbox_events(processed_at, available_at);

Code 4: Repository-Test mit Testcontainers

package com.seb4u.demo.spring.commerceflow.orders.adapter.jdbc;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest;
import org.springframework.context.annotation.Import;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

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

@JdbcTest
@Testcontainers
@Import(JdbcOrderRepository.class)
class JdbcOrderRepositoryTest {
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17-alpine");

    @Autowired JdbcOrderRepository orders;

    @Test
    void savesAndLoadsOrderWithVersionContract() {
        var order = TestOrders.pending("o-100", "c-42");
        orders.insert(order);

        var loaded = orders.findByIdForUpdate("o-100");

        assertThat(loaded).isPresent();
        assertThat(loaded.get().version()).isEqualTo(0);
    }
}
🧪

Testing Masterclass: Unit, Integration, Contract, Architektur

Modul F · Enterprise Java: Spring, Persistence, Testing, Security & Production · Alt-Referenz: K68

Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.

Testpyramide für Java-Enterprise Testpyramide für Java-Enterprise Viele schnelle Unit-Tests Use-Case-Tests mit Ports/Fakes Repository/HTTP-Integrationstests Contract-Tests Wenige End-to-End-Tests

Professionelles Testing bewertet Risiko. Du testest nicht „Klassen“, sondern Verträge: fachliche Invarianten, Transaktionsgrenzen, API-Fehlerformate, Mapping, Migrationen und Nebenläufigkeit.

Anti-Pattern: 100% Coverage durch triviale Getter-Tests ist wertlos. Ein guter Test bricht, wenn eine relevante Regel verletzt wird.

Code 1: Domain-Test ohne Spring Context

class OrderDomainTest {
    @Test
    void confirmedOrderCannotBeConfirmedTwice() {
        var order = Order.draft("o-1", CustomerId.of("c-1"));
        order.addLine(Sku.of("BOOK-1"), Quantity.of(2));
        order.confirm("reservation-1");

        var ex = assertThrows(DomainRuleViolation.class, () -> order.confirm("reservation-2"));

        assertThat(ex.code()).isEqualTo("ORDER_ALREADY_CONFIRMED");
        assertThat(order.pullDomainEvents())
                .extracting(DomainEvent::type)
                .containsExactly("OrderConfirmed");
    }
}

Code 2: WebMvcTest für Fehlervertrag

package com.seb4u.demo.spring;

@WebMvcTest(OrderController.class)
class OrderControllerValidationTest {
    @Autowired MockMvc mvc;
    @MockBean PlaceOrderUseCase placeOrder;

    @Test
    void returnsProblemDetailForInvalidRequest() throws Exception {
        mvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"customerId":"", "lines":[{"sku":"", "quantity":0}]}
                    """))
           .andExpect(status().isBadRequest())
           .andExpect(jsonPath("$.type").value("urn:problem:validation"))
           .andExpect(jsonPath("$.errors.customerId").exists())
           .andExpect(jsonPath("$.errors['lines[0].sku']").exists());
    }
}

Code 3: Architekturtest mit ArchUnit

package com.seb4u.demo.spring;

@AnalyzeClasses(packages = "com.seb4u.demo.spring.commerceflow")
class ArchitectureTest {
    @ArchTest
    static final ArchRule domain_must_not_depend_on_spring = noClasses()
            .that().resideInAPackage("..domain..")
            .should().dependOnClassesThat().resideInAnyPackage("org.springframework..", "jakarta.persistence..");

    @ArchTest
    static final ArchRule controllers_must_not_access_repositories = noClasses()
            .that().resideInAPackage("..api..")
            .should().accessClassesThat().resideInAPackage("..adapter.jdbc..");
}

Code 4: Contract-Test für externe Payment API

class PaymentClientContractTest {
    WireMockServer paymentApi = new WireMockServer(options().dynamicPort());

    @BeforeEach void start() { paymentApi.start(); }
    @AfterEach void stop() { paymentApi.stop(); }

    @Test
    void mapsDeclinedPaymentToTypedResult() {
        paymentApi.stubFor(post("/payments")
                .willReturn(jsonResponse("""
                    {"status":"DECLINED","reason":"INSUFFICIENT_FUNDS"}
                    """, 402)));

        var client = new PaymentClient(paymentApi.baseUrl(), Duration.ofSeconds(2));
        var result = client.authorize(new PaymentRequest("order-1", Money.eur("49.90")));

        assertThat(result).isInstanceOf(PaymentResult.Declined.class);
        assertThat(((PaymentResult.Declined) result).reason()).isEqualTo("INSUFFICIENT_FUNDS");
    }
}
🛡️

Security Deep Dive: Auth, JWT, OWASP, Secrets

Modul F · Enterprise Java: Spring, Persistence, Testing, Security & Production · Alt-Referenz: K69

Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.

Security Request Flow Security Request Flow TLS Filter Chain Authentication Authorization Validation Audit Log

Security ist kein Extra-Kapitel, sondern ein Querschnitt. Jede Grenze braucht Eingabevalidierung, jede Identität einen Kontext, jede Berechtigung eine fachliche Entscheidung, und jedes Secret einen sicheren Speicherort.

RisikoSchutzCode-OrtFehler
InjectionPrepared Statements, ValidationRepository / API DTOString-Konkatenation in SQL
Broken Access ControlFachliche PoliciesApplication ServiceNur URL-Rollen prüfen
Credential LeakHashing, Secret ManagerIdentity AdapterPasswörter loggen
JWT MissbrauchIssuer, Audience, Expiry, Key RotationSecurity ConfigToken ohne Claims prüfen

Code 1: Spring Security Konfiguration mit klaren Grenzen

@Configuration
@EnableMethodSecurity
class SecurityConfig {
    @Bean
    SecurityFilterChain api(HttpSecurity http) throws Exception {
        return http
                .csrf(csrf -> csrf.disable())
                .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/actuator/health", "/actuator/info").permitAll()
                        .requestMatchers(HttpMethod.POST, "/api/orders").hasAuthority("SCOPE_orders:write")
                        .requestMatchers(HttpMethod.GET, "/api/orders/**").hasAuthority("SCOPE_orders:read")
                        .anyRequest().denyAll())
                .oauth2ResourceServer(oauth -> oauth.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter())))
                .build();
    }

    @Bean
    Converter<Jwt, AbstractAuthenticationToken> jwtConverter() {
        var converter = new JwtGrantedAuthoritiesConverter();
        converter.setAuthorityPrefix("SCOPE_");
        converter.setAuthoritiesClaimName("scope");
        return jwt -> new JwtAuthenticationToken(jwt, converter.convert(jwt), jwt.getSubject());
    }
}

Code 2: Fachliche Policy statt nur Annotation

package com.seb4u.demo.spring;

@Component
final class OrderAccessPolicy {
    boolean canReadOrder(UserPrincipal user, Order order) {
        if (user.hasRole("ADMIN")) return true;
        if (user.hasRole("SUPPORT") && order.isNotDeleted()) return true;
        return order.customerId().equals(user.customerId());
    }

    void requireCanRead(UserPrincipal user, Order order) {
        if (!canReadOrder(user, order)) {
            throw new AccessDeniedException("User may not read order " + order.id());
        }
    }
}

@Service
class GetOrderService {
    private final OrderRepository orders;
    private final OrderAccessPolicy policy;

    GetOrderService(OrderRepository orders, OrderAccessPolicy policy) {
        this.orders = orders; this.policy = policy;
    }

    @Transactional(readOnly = true)
    public OrderView get(String id, UserPrincipal user) {
        var order = orders.findById(id).orElseThrow(NotFoundException::new);
        policy.requireCanRead(user, order);
        return OrderView.from(order);
    }
}

Code 3: Passwort-Hashing mit PasswordEncoder und Upgrade-Pfad

package com.seb4u.demo.spring;

@Service
class PasswordService {
    private final PasswordEncoder encoder;
    private final UserRepository users;

    PasswordService(PasswordEncoder encoder, UserRepository users) {
        this.encoder = encoder; this.users = users;
    }

    public boolean verifyAndUpgradeHash(String username, char[] rawPassword) {
        var user = users.findByUsername(username).orElseThrow(AuthenticationFailed::new);
        var password = new String(rawPassword);
        try {
            if (!encoder.matches(password, user.passwordHash())) return false;
            if (encoder.upgradeEncoding(user.passwordHash())) {
                users.updatePasswordHash(user.id(), encoder.encode(password));
            }
            return true;
        } finally {
            Arrays.fill(rawPassword, '\0');
        }
    }
}
🚀

Production Readiness: Logs, Metrics, Health, Docker, CI/CD

Modul F · Enterprise Java: Spring, Persistence, Testing, Security & Production · Alt-Referenz: K70

Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.

Production Readiness Loop Production Readiness Loop Config Build Test Package Deploy Observe Rollback

Production Readiness heißt: Die Anwendung startet reproduzierbar, liest Konfiguration sicher, kann überwacht werden, erklärt Fehler durch Logs/Metriken/Traces und kann bei Problemen zurückgerollt werden.

Master-Regel: Alles, was du nicht beobachten kannst, kannst du in Production nicht sicher betreiben.

Code 1: Actuator Health Indicator für kritische Abhängigkeit

@Component
class OutboxHealthIndicator implements HealthIndicator {
    private final OutboxRepository outbox;

    OutboxHealthIndicator(OutboxRepository outbox) { this.outbox = outbox; }

    @Override
    public Health health() {
        var lag = outbox.oldestUnprocessedAge();
        var pending = outbox.pendingCount();
        var builder = lag.compareTo(Duration.ofMinutes(5)) > 0 ? Health.down() : Health.up();
        return builder
                .withDetail("pendingEvents", pending)
                .withDetail("oldestPendingAgeSeconds", lag.toSeconds())
                .build();
    }
}

Code 2: Strukturierte Logs mit Korrelation

@Component
class CorrelationIdFilter extends OncePerRequestFilter {
    static final String HEADER = "X-Correlation-Id";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String correlationId = Optional.ofNullable(request.getHeader(HEADER))
                .filter(id -> id.matches("[a-zA-Z0-9._-]{8,80}"))
                .orElse(UUID.randomUUID().toString());
        MDC.put("correlationId", correlationId);
        response.setHeader(HEADER, correlationId);
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.remove("correlationId");
        }
    }
}

Code 3: Dockerfile für reproduzierbaren Runtime-Container

# syntax=docker/dockerfile:1
FROM eclipse-temurin:25-jre-alpine AS runtime
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY build/libs/commerceflow.jar /app/commerceflow.jar
USER app
EXPOSE 8080
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/commerceflow.jar"]

Code 4: GitHub Actions CI Pipeline

name: commerceflow-ci
on: [push, pull_request]
jobs:
  build-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '25'
          cache: gradle
      - run: ./gradlew clean test integrationTest jacocoTestReport --scan
      - run: ./gradlew bootJar
      - uses: actions/upload-artifact@v4
        with:
          name: commerceflow-jar
          path: build/libs/*.jar
🏬

CommerceFlow Enterprise Upgrade

Modul F · Enterprise Java: Spring, Persistence, Testing, Security & Production · Alt-Referenz: K71

Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.

CommerceFlow Enterprise Architektur CommerceFlow Enterprise Architektur API Layer: REST + ProblemDetails Application Layer: Use Cases + Policies Domain Layer: Order, Payment, Inventory Persistence Adapter: PostgreSQL + Flyway Async Adapter: Outbox Worker Operations: Actuator + Logs + Metrics

Das bestehende Master-Projekt wird hier zu einer Enterprise-Version erweitert. Die zentrale Idee: CommerceFlow bleibt fachlich sauber, während Spring Boot nur die äußeren Adapter und den Betrieb erleichtert.

Code 1: Package-Struktur

com.seb4u.demo.spring.commerceflow
├── CommerceFlowApplication.java
├── orders
│   ├── api               // REST DTOs, Controller, Problem Mapping
│   ├── app               // Use Cases, Ports, Transaction Scripts
│   ├── domain            // Aggregates, Value Objects, Events
│   └── adapter
│       ├── jdbc          // Repository Implementierung
│       └── outbox        // Event Publisher
├── identity
│   ├── api
│   ├── app
│   └── adapter
└── shared
    ├── errors
    ├── security
    └── observability

Code 2: Idempotenter Command Handler

package com.seb4u.demo.spring;

@Service
class IdempotentPlaceOrderHandler {
    private final IdempotencyRepository idempotency;
    private final PlaceOrderUseCase delegate;
    private final ObjectMapper json;

    @Transactional
    public PlaceOrderResponse handle(String key, PlaceOrderCommand command) {
        var hash = Hashing.sha256(json.writeValueAsBytes(command));
        var previous = idempotency.find(key);
        if (previous.isPresent()) {
            if (!previous.get().requestHash().equals(hash)) {
                throw new IdempotencyConflict("Same idempotency key used for different request");
            }
            return json.readValue(previous.get().responseJson(), PlaceOrderResponse.class);
        }

        var response = delegate.handle(command);
        idempotency.save(new IdempotencyRecord(key, hash, json.writeValueAsString(response)));
        return response;
    }
}

Code 3: Outbox Worker mit Scheduling und Locking

@Component
class OutboxWorker {
    private final OutboxRepository outbox;
    private final EventPublisher publisher;
    private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

    @Scheduled(fixedDelayString = "${commerceflow.outbox.poll-interval:2000}")
    void publishBatch() {
        var batch = outbox.claimNextBatch(50, Duration.ofMinutes(2));
        var futures = batch.stream()
                .map(event -> CompletableFuture.runAsync(() -> publishOne(event), executor))
                .toList();
        futures.forEach(CompletableFuture::join);
    }

    private void publishOne(OutboxEvent event) {
        try {
            publisher.publish(event);
            outbox.markProcessed(event.id());
        } catch (Exception ex) {
            outbox.markFailed(event.id(), ex.getClass().getSimpleName(), ex.getMessage());
        }
    }
}
📋

Enterprise Runbook & Release Checklist

Modul F · Enterprise Java: Spring, Persistence, Testing, Security & Production · Alt-Referenz: K72

Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.

Release Gate Release Gate Branch Build Tests Security Scan Migration Dry Run Deploy Smoke Test

Ein Runbook ist die Brücke zwischen Code und Betrieb. Es beantwortet: Wie starte ich das System? Wie erkenne ich Fehler? Wie rolle ich zurück? Welche Metriken sind kritisch?

GateFrageAkzeptanzkriterium
BuildIst das Artefakt reproduzierbar?Clean Build auf CI, Lockfile/BOM, keine lokalen Pfade
TestSind Kernrisiken getestet?Domain, Persistence, API, Security, Migration
DeployKann die Version sicher starten?Health UP, Migration erfolgreich, Config validiert
ObserveKann man Fehler diagnostizieren?Korrelation, strukturierte Logs, Metriken, Alerts
RollbackIst Rückkehr möglich?DB-Migration kompatibel oder Rollback-Plan

Code 1: application.yml Production Template

spring:
  application:
    name: commerceflow
  datasource:
    url: ${DB_URL}
    username: ${DB_USER}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
      connection-timeout: 2000
  flyway:
    enabled: true
    locations: classpath:db/migration
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      probes:
        enabled: true
logging:
  pattern:
    level: "%5p [correlationId=%X{correlationId:-none}]"
commerceflow:
  outbox:
    worker-threads: 16
    poll-interval: 2s

Code 2: Smoke-Test nach Deployment

#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${1:-http://localhost:8080}"

curl -fsS "$BASE_URL/actuator/health/readiness" | jq -e '.status == "UP"'
curl -fsS "$BASE_URL/actuator/info" | jq -e '.app.version != null'

ORDER_ID=$(curl -fsS -X POST "$BASE_URL/api/orders" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: smoke-$(date +%s)" \
  -d '{"customerId":"smoke-customer","lines":[{"sku":"DEMO-1","quantity":1}]}' | jq -r '.id')

test -n "$ORDER_ID" && echo "Smoke order created: $ORDER_ID"
Modul G

🎯 Job-Ready, Aufgaben, Code Review & Interview

Aufgaben mit Lösungen, Review-Fallen, Senior-Interviewfragen, Architektur-Cases und Portfolio-Pack.

8Themen in diesem Modul
14Codeblöcke
4SVG-Grafiken
💼

Job-Ready Edition Überblick

Modul G · Job-Ready, Aufgaben, Code Review & Interview · Alt-Referenz: K73

Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.

Vom Handbuch zum Bewerbungs- und Interviewtraining

Diese Edition verwandelt den Lernstoff in überprüfbare Leistung: Aufgaben, Musterlösungen, Code-Review-Fallen, Architektur-Cases, Senior-Fragen und Portfolio-Bausteine.

Aufgaben + LösungenCode ReviewSenior InterviewPortfolio
Job-Ready Kompetenzmodell Job-Ready Kompetenzmodell Ich kann es erklären Ich kann es implementieren Ich kann Fehler finden Ich kann Trade-offs begründen Ich kann es production-ready machen
KompetenzJuniorMid-LevelSenior
CodeFeature umsetzenRobuste APIs designenKomplexität reduzieren
DebuggingFehler lokal findenSystematisch eingrenzenProduktionsdiagnose planen
ArchitekturLayer erkennenBoundaries umsetzenTrade-offs erklären
DeliveryTests schreibenCI stabil haltenRisiko und Rollout steuern
🧠

Aufgaben & Lösungen: Core Java Mastery

Modul G · Job-Ready, Aufgaben, Code Review & Interview · Alt-Referenz: K74

Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.

Diese Aufgaben prüfen, ob du Core Java nicht nur syntaktisch, sondern als Design-Werkzeug beherrschst.

Kata 1: Money Value Object Advanced

Implementiere ein unveränderliches Money-Objekt mit Währung, Rundungsregeln, Addition, Vergleich und Parser.

Musterlösung

public record Money(BigDecimal amount, Currency currency) implements Comparable<Money> {
    public Money {
        Objects.requireNonNull(amount, "amount");
        Objects.requireNonNull(currency, "currency");
        amount = amount.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_EVEN);
    }

    public static Money parse(String value, String currencyCode) {
        return new Money(new BigDecimal(value), Currency.getInstance(currencyCode));
    }

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

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

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

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

Kata 2: Sealed Result Contract Master

Ersetze null/Exception-gesteuerte Service-Rückgaben durch typisierte Ergebnisse.

public sealed interface CreateAccountResult permits CreateAccountResult.Created, CreateAccountResult.Rejected {
    record Created(AccountId id, Instant createdAt) implements CreateAccountResult {}
    record Rejected(String code, String message, Map<String, String> fields) implements CreateAccountResult {}

    default <T> T fold(Function<Rejected, T> onRejected, Function<Created, T> onCreated) {
        return switch (this) {
            case Rejected r -> onRejected.apply(r);
            case Created c -> onCreated.apply(c);
        };
    }
}
Keine primitiven Geldbeträge ohne Währung.
Keine null-Rückgaben an Service-Grenzen.
Keine Framework-Abhängigkeit im Domain-Core.
🧵

Aufgaben & Lösungen: I/O und Concurrency

Modul G · Job-Ready, Aufgaben, Code Review & Interview · Alt-Referenz: K75

Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.

Import-Kata Flow Import-Kata Flow Watch Directory Validate File Parse Stream Persist Batch Publish Event Archive

Kata 1: Robuster Dateiimport Master

Schreibe einen Importer, der große Dateien streaming-basiert verarbeitet, fehlerhafte Dateien quarantiniert und erfolgreiche Dateien atomar archiviert.

Musterlösung: Import-Orchestrator

public final class FileImportOrchestrator {
    private final CsvOrderParser parser;
    private final OrderBatchSink sink;
    private final Path archive;
    private final Path quarantine;

    public ImportReport importFile(Path file) {
        var started = Instant.now();
        try (var lines = Files.lines(file, StandardCharsets.UTF_8)) {
            var report = parser.parse(lines)
                    .collect(BatchCollectors.persistingInBatches(500, sink));
            moveAtomically(file, archive.resolve(file.getFileName()));
            return report.finished(started, Instant.now());
        } catch (Exception ex) {
            moveBestEffort(file, quarantine.resolve(file.getFileName() + ".failed"));
            return ImportReport.failed(file, ex.getMessage(), started, Instant.now());
        }
    }

    private static void moveAtomically(Path source, Path target) throws IOException {
        Files.createDirectories(target.getParent());
        Files.move(source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
    }

    private static void moveBestEffort(Path source, Path target) {
        try { moveAtomically(source, target); } catch (IOException ignored) { }
    }
}

Kata 2: Concurrency Bulkhead Senior

Begrenze parallele Calls zu einem instabilen Fremdsystem und sorge für Timeouts, Fallbacks und saubere Cancellation.

public final class BulkheadedClient implements AutoCloseable {
    private final ExternalClient delegate;
    private final Semaphore permits;
    private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

    public BulkheadedClient(ExternalClient delegate, int maxConcurrent) {
        this.delegate = delegate;
        this.permits = new Semaphore(maxConcurrent);
    }

    public CompletableFuture<ClientResult> call(Request request) {
        return CompletableFuture.supplyAsync(() -> {
            boolean acquired = false;
            try {
                acquired = permits.tryAcquire(200, TimeUnit.MILLISECONDS);
                if (!acquired) return ClientResult.rejected("BULKHEAD_FULL");
                return delegate.call(request).orTimeout(Duration.ofSeconds(2));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return ClientResult.rejected("INTERRUPTED");
            } catch (Exception e) {
                return ClientResult.failed(e.getClass().getSimpleName());
            } finally {
                if (acquired) permits.release();
            }
        }, executor).orTimeout(3, TimeUnit.SECONDS);
    }

    @Override public void close() { executor.shutdownNow(); }
}
Review-Falle: `parallelStream()` ist keine allgemeine Lösung für I/O-Parallelität. Nutze kontrollierte Executor, Timeouts und Backpressure.
🌐

Aufgaben & Lösungen: Spring, Data, Security

Modul G · Job-Ready, Aufgaben, Code Review & Interview · Alt-Referenz: K76

Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.

Kata 1: Sichere Order API Senior

Baue eine REST API mit Validation, Idempotency-Key, ProblemDetails und rollenbasierter Autorisierung.

package com.seb4u.demo.spring;

@RestController
@RequestMapping("/api/v1/orders")
class SecureOrderApi {
    private final IdempotentPlaceOrderHandler handler;
    private final CurrentUser currentUser;

    @PostMapping
    @PreAuthorize("hasAuthority('SCOPE_orders:write')")
    ResponseEntity<?> place(
            @RequestHeader("Idempotency-Key") @Pattern(regexp = "[a-zA-Z0-9._-]{12,128}") String key,
            @Valid @RequestBody PlaceOrderDto dto) {
        var user = currentUser.requireAuthenticated();
        var command = dto.toCommand(user.customerId());
        var response = handler.handle(key, command);
        return ResponseEntity.created(URI.create("/api/v1/orders/" + response.orderId())).body(response);
    }
}

Kata 2: Transaction + Outbox Master

Speichere fachliche Änderung und Integrationsevent atomar in derselben Datenbanktransaktion.

package com.seb4u.demo.spring;

@Service
class PayOrderService {
    private final OrderRepository orders;
    private final OutboxRepository outbox;

    @Transactional
    public PayOrderResult pay(String orderId, PaymentReceipt receipt) {
        var order = orders.findByIdForUpdate(orderId).orElseThrow(NotFoundException::new);
        order.markPaid(receipt.paymentId(), receipt.amount());
        orders.save(order);
        order.pullDomainEvents().forEach(event -> outbox.insert(OutboxEvent.from(event)));
        return PayOrderResult.paid(order.id(), order.version());
    }
}

Test der Transaktionsgarantie

@Test
void rollsBackOrderWhenOutboxInsertFails() {
    var order = fixtures.persistPendingOrder();
    outbox.failNextInsert();

    assertThrows(DataAccessException.class, () -> service.pay(order.id(), fixtures.receipt()));

    assertThat(orders.findById(order.id()).get().status()).isEqualTo(OrderStatus.PENDING);
    assertThat(outbox.eventsFor(order.id())).isEmpty();
}
🔍

Code-Review-Fallen: Schlechter Code zu professionellem Code

Modul G · Job-Ready, Aufgaben, Code Review & Interview · Alt-Referenz: K77

Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.

Ein starkes Interview prüft oft nicht, ob du perfekte Syntax schreibst, sondern ob du Fehler, Risiken und Designgeruch erkennst.

Falle 1: Controller mit Businesslogik

// Schlecht: Controller validiert fachliche Regeln, speichert direkt und kennt SQL-Details.
@PostMapping("/orders")
public String create(@RequestBody Map<String, Object> body) {
    if (((List<?>) body.get("lines")).isEmpty()) return "no lines";
    jdbc.update("insert into orders ...");
    return "ok";
}
Besser: DTO validieren, Command erzeugen, Use Case aufrufen, Ergebnis in HTTP übersetzen.
package com.seb4u.demo.spring;

@PostMapping("/orders")
ResponseEntity<?> create(@Valid @RequestBody CreateOrderRequest request) {
    return placeOrder.handle(request.toCommand()).fold(
            error -> ProblemResponses.badRequest(error),
            created -> ResponseEntity.created(URI.create("/orders/" + created.id())).body(created)
    );
}

Falle 2: Unsicheres SQL

// Schlecht
var sql = "select * from users where email = '" + email + "'";
return jdbc.queryForObject(sql, mapper);

// Besser
return jdbc.query("select id,email,role from users where email = ?", mapper, email)
           .stream().findFirst();

Falle 3: Nebenläufigkeit ohne Timeouts

// Schlecht: join ohne Timeout kann Request-Threads blockieren.
var user = userFuture.join();
var orders = ordersFuture.join();

// Besser: explizite Deadline und fachlicher Fallback.
var user = userFuture.orTimeout(800, MILLISECONDS).exceptionally(ex -> UserView.anonymous()).join();
var orders = ordersFuture.orTimeout(1200, MILLISECONDS).exceptionally(ex -> List.of()).join();
🎙️

Senior Java Interviewfragen mit Musterantworten

Modul G · Job-Ready, Aufgaben, Code Review & Interview · Alt-Referenz: K78

Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.

FrageWorauf Interviewer achtenStarke Antwort
Wann nutzt du Records?Immutability, Value SemanticsFür transparente Datenwerte, DTOs und Value Objects; nicht für komplexe Aggregate mit Identität und Lebenszyklus.
Wie vermeidest du N+1?ORM-VerständnisFetch Join, Entity Graph, DTO Query, Batch Size, SQL Logging und Testdaten mit mehreren Relationen.
Was ist eine Transaktionsgrenze?Use-Case-DenkenEine fachlich atomare Änderung. Nicht Controller, nicht Repository-Methode, sondern Application Service.
Virtual Threads vs Platform Threads?Concurrency-ModellVirtual Threads sind gut für blockierendes I/O; sie ersetzen nicht Synchronisationsdesign, Backpressure oder CPU-Pools.

Live-Coding-Frage: LRU Cache mit LinkedHashMap

public final class LruCache<K, V> {
    private final int maxSize;
    private final Map<K, V> map;

    public LruCache(int maxSize) {
        if (maxSize < 1) throw new IllegalArgumentException("maxSize must be positive");
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<>(16, 0.75f, true) {
            @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
                return size() > LruCache.this.maxSize;
            }
        };
    }

    public synchronized Optional<V> get(K key) { return Optional.ofNullable(map.get(key)); }
    public synchronized void put(K key, V value) { map.put(key, value); }
    public synchronized int size() { return map.size(); }
}

System-Design-Frage: Payment Retry

Musterantwort: Payment-Autorisierung braucht Idempotency-Key, Timeout, Retry nur bei transienten Fehlern, Dead-letter für dauerhafte Fehler, Audit-Log, keine Doppelbuchung und klare Statusmaschine.
Payment Retry Design Payment Retry Design Order Pending Authorize Payment Transient Error Retry with Backoff Dead Letter Manual Review
🏛️

Architektur-Cases für Senior-Gespräche

Modul G · Job-Ready, Aufgaben, Code Review & Interview · Alt-Referenz: K79

Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.

Case 1: Monolith modularisieren Senior

Ein Team hat einen großen Spring Boot Monolithen mit zyklischen Abhängigkeiten. Ziel: keine Microservices sofort, sondern Modulgrenzen stabilisieren.

Modularisierung ohne Microservice-Hype Modularisierung ohne Microservice-Hype Ist-Zustand messen: Dependencies, DB-Zugriffe, Hotspots Bounded Contexts schneiden Ports zwischen Modulen definieren ArchUnit Regeln einführen Datenbesitz klären Erst danach Deployment-Schnitt prüfen
EntscheidungGute BegründungWarnsignal
Modularer MonolithEin Deployment, klare CodegrenzenTeams blockieren sich ständig
MicroserviceUnabhängige Skalierung und OwnershipVerteilte Transaktionen nötig
Event-DrivenLose Kopplung, Audit-TrailFehlerhafte Idempotenz

Architecture Decision Record

# ADR-007: Orders und Payments bleiben im modularen Monolithen

## Kontext
Orders und Payments teilen Transaktionsdaten und werden vom selben Team betrieben.

## Entscheidung
Wir trennen die Packages und Datenzugriffe strikt, behalten aber ein gemeinsames Deployment.
Kommunikation erfolgt über Application Ports und Domain Events innerhalb desselben Prozesses.

## Konsequenzen
+ weniger verteilte Komplexität
+ klare Modulregeln mit ArchUnit testbar
- Deployment bleibt gekoppelt
- spätere Extraktion braucht Outbox und API-Vertrag

Case 2: Langsame API Master

Eine Order-Übersichtsseite lädt langsam. Du musst systematisch vorgehen.

1️⃣ Messung: P95/P99, DB Queries, Thread Dumps, externe Calls.
2️⃣ Hypothese: N+1, fehlender Index, langsamer Remote Call, Pool-Erschöpfung.
3️⃣ Experiment: SQL Log, EXPLAIN, Load Test, Profiling.
4️⃣ Fix: DTO Query, Index, Cache, Pagination, Timeout.
📁

Portfolio- und Bewerbungs-Pack

Modul G · Job-Ready, Aufgaben, Code Review & Interview · Alt-Referenz: K80

Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.

Ein gutes Java-Portfolio zeigt nicht nur Code, sondern Engineering-Reife: README, Architekturentscheidungen, Tests, CI, Docker, Runbook und bekannte Trade-offs.

ArtefaktWas es beweistMindestqualität
READMEKommunikationSetup, Features, Architekturdiagramm, Commands
TestsQualitätsdenkenUnit + Integration + API Tests
Docker ComposeReproduzierbarkeitApp + Postgres + optional Redis/Kafka
ADRArchitekturdenkenMindestens 3 Entscheidungen mit Konsequenzen
RunbookProduction MindsetHealth, Logs, Metrics, Troubleshooting

README-Vorlage

# CommerceFlow

Professionelles Java/Spring-Boot Projekt für Order, Payment und Outbox Workflows.

## Features
- REST API mit ProblemDetails und Validation
- PostgreSQL Persistence mit Flyway Migrationen
- Transactional Outbox Worker
- JWT Resource Server Security
- Unit, Integration und Architekturtests
- Docker Compose für lokale Umgebung

## Architektur
```text
API -> Application Use Cases -> Domain -> Ports -> Adapters
```

## Lokal starten
```bash
./gradlew clean test
./gradlew bootRun
```

## Production Readiness
- /actuator/health/readiness
- strukturierte Logs mit correlationId
- idempotente POST Requests
- Rollback-Hinweise in docs/runbook.md

Bewerbungsprojekt-Pitch

„Ich habe CommerceFlow gebaut, um Enterprise-Java nicht nur mit CRUD zu zeigen, sondern mit Transaktionen, Outbox, Security, Integration Tests, Architekturregeln und Production Readiness. Besonders wichtig war mir, Domain-Code frameworkfrei zu halten und Spring Boot als Adapter- und Betriebsplattform zu nutzen.“

30-Tage Job-Ready Plan

WocheFokusOutput
1Core Java + Tests20 Katas, 1 Refactoring
2Spring + PersistenceREST API, DB, Migrationen
3Security + ProductionJWT, Actuator, Docker, CI
4Interview + PortfolioREADME, ADRs, System Design Antworten
Modul H

🌐 Fullstack Java Edition

REST API, Thymeleaf, Frontend-Integration, Login/Register, Upload, Export, Reporting, UX und Fehlerseiten.

7Themen in diesem Modul
12Codeblöcke
3SVG-Grafiken
🌐
Finale Erweiterung

Fullstack Java Edition Überblick

Modul H · Fullstack Java Edition · Alt-Referenz: K82

Backend, UI, API-Verträge und produktionsnahe Webfunktionen in einen Lernpfad bringen.

Fullstack-Java Architektur Fullstack-Java Architektur Browser/UI Thymeleaf oder SPA REST API DTOs, Validation, Problem Details Application Layer Use Cases, Transaktionen Domain Core Records, Events, Invarianten Persistence JDBC/JPA, Migrations Operations Logs, Metrics, Health Fullstack heißt: UI und Backend teilen klare Verträge, aber keine chaotischen Abhängigkeiten.

Serverseitige UI

Thymeleaf ist schnell für Admin-Backoffice, interne Tools und klassische Formularanwendungen.

Clientseitige UI

SPA/Fetch/React/Vue eignet sich für interaktive Oberflächen, braucht aber klare API-Kontrakte.

Backend-Vertrag

DTOs, Validation und Problem Details verhindern, dass Domain-Objekte direkt nach außen leaken.

Produktionsblick

Upload, Export, Pagination, Auth und Fehlerseiten sind Pflicht für echte Anwendungen.

EntscheidungNimm dasWenn
Admin/BackofficeSpring MVC + ThymeleafFormulare, Tabellen, Rollen, schnelle Entwicklung.
Public APIREST + JSON + Problem DetailsMehrere Clients, mobile Apps, externe Integrationen.
Interaktive UISPA mit fetch/React/VueViele UI-Zustände, Live-Validierung, dynamische Komponenten.
ReportingStreaming CSV/PDF ExportGroße Datenmengen ohne alles in Memory zu laden.
Anti-Pattern: Domain-Entities direkt im Controller zurückgeben. Besser: DTOs, explizite Mapper und ein stabiler API-Vertrag.
🔌
Finale Erweiterung

Fullstack REST API Deep Dive

Modul H · Fullstack Java Edition · Alt-Referenz: K83

Professionelle API-Schicht mit DTOs, Validation, Fehlerverträgen und Query-Parametern.

REST API als stabiler Vertrag

Spring MVCDTO MappingValidationProblemDetailPagination

Die API-Schicht nimmt externe Daten entgegen, validiert sie und übersetzt sie in Commands. Sie soll nicht wissen, wie Persistence intern funktioniert.

Komplexes Beispiel 1: Order REST Controller mit Validation, Result-Switch und Problem Details

Zeigt einen produktionsnahen Controller mit DTOs, Idempotency-Key und expliziten Fehlerantworten.

package com.seb4u.demo.spring.commerceflow.web;

import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.net.URI;
import java.time.Instant;
import java.util.*;

@RestController
@RequestMapping("/api/orders")
final class OrderRestController {
    private final PlaceOrderUseCase placeOrder;
    private final OrderQueryService queries;

    OrderRestController(PlaceOrderUseCase placeOrder, OrderQueryService queries) {
        this.placeOrder = placeOrder;
        this.queries = queries;
    }

    @PostMapping
    ResponseEntity<?> place(@Valid @RequestBody PlaceOrderRequest request) {
        var command = new PlaceOrderCommand(
            new CustomerId(request.customerId()),
            request.lines().stream()
                .map(l -> new OrderLine(new Sku(l.sku()), l.quantity(), Money.eur(l.price())))
                .toList(),
            IdempotencyKey.from(request.idempotencyKey())
        );

        return switch (placeOrder.handle(command)) {
            case PlaceOrderResult.Accepted ok -> ResponseEntity
                .created(URI.create("/api/orders/" + ok.orderId().value()))
                .body(new OrderResponse(ok.orderId().value(), "ACCEPTED", ok.acceptedAt()));
            case PlaceOrderResult.Duplicate dup -> ResponseEntity.status(HttpStatus.CONFLICT)
                .body(problem("duplicate-order", "Request was already processed", dup.orderId().value()));
            case PlaceOrderResult.Rejected bad -> ResponseEntity.badRequest()
                .body(problem("invalid-order", bad.reason(), null));
        };
    }

    @GetMapping
    PageDto<OrderSummaryDto> find(
            @RequestParam Optional<String> customer,
            @RequestParam(defaultValue = "0") @Min(0) int page,
            @RequestParam(defaultValue = "25") @Min(1) @Max(100) int size,
            @RequestParam(defaultValue = "createdAt,desc") String sort) {
        return queries.search(new OrderSearch(customer.map(CustomerId::new), page, size, SortSpec.parse(sort)));
    }

    private static ProblemDetail problem(String type, String detail, String instance) {
        var p = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
        p.setType(URI.create("https://commerceflow.local/problems/" + type));
        p.setTitle(type);
        p.setDetail(detail);
        if (instance != null) p.setInstance(URI.create("/api/orders/" + instance));
        return p;
    }
}

record PlaceOrderRequest(
    @NotBlank String customerId,
    @NotBlank String idempotencyKey,
    @NotEmpty List<LineRequest> lines) {}

record LineRequest(@NotBlank String sku, @Min(1) int quantity, @DecimalMin("0.01") BigDecimal price) {}
record OrderResponse(String id, String status, Instant acceptedAt) {}
Komplexes Beispiel 2: Sichere Pagination, Sorting und Filter-Spezifikation

Verhindert SQL-Injection bei Sortierfeldern durch Whitelist-Mapping und kapselt Paging-Regeln.

record PageRequest(int page, int size, List<SortField> sort) {
    PageRequest {
        if (page < 0) throw new IllegalArgumentException("page must be >= 0");
        if (size < 1 || size > 100) throw new IllegalArgumentException("size must be 1..100");
        sort = List.copyOf(sort);
    }
    int offset() { return Math.multiplyExact(page, size); }
}

record SortField(String property, Direction direction) {
    enum Direction { ASC, DESC }
}

final class OrderSqlBuilder {
    private static final Map<String, String> SORT_COLUMNS = Map.of(
        "createdAt", "created_at", "total", "total_amount", "customer", "customer_id");

    SqlAndParams build(OrderSearch search, PageRequest page) {
        var where = new ArrayList<String>();
        var params = new ArrayList<Object>();
        search.customer().ifPresent(c -> { where.add("customer_id = ?"); params.add(c.value()); });
        search.state().ifPresent(s -> { where.add("state = ?"); params.add(s.name()); });

        String orderBy = page.sort().stream()
            .map(s -> SORT_COLUMNS.getOrDefault(s.property(), "created_at") + " " + s.direction().name())
            .collect(Collectors.joining(", "));

        String sql = "select * from orders" +
            (where.isEmpty() ? "" : " where " + String.join(" and ", where)) +
            " order by " + orderBy + " limit ? offset ?";
        params.add(page.size()); params.add(page.offset());
        return new SqlAndParams(sql, params);
    }
}

record SqlAndParams(String sql, List<Object> params) {}
Master-Regel: Ein Controller darf orchestrieren, aber nicht entscheiden. Geschäftsregeln gehören in Use Cases und Domain-Services.
🖥️
Finale Erweiterung

Serverseitige UI mit Thymeleaf

Modul H · Fullstack Java Edition · Alt-Referenz: K84

Backoffice-Seiten mit klaren Formularmodellen, PRG-Pattern und sicheren Aktionen bauen.

Thymeleaf Backoffice Flow Thymeleaf Backoffice Flow GET Formular Filtermodell bauen Service Query Page laden Template Render Tabelle, Filter, Aktionen POST Action Use Case auslösen Flash Message Redirect + Ergebnis Audit Log Aktion nachvollziehbar machen Serverseitige UI ist besonders stark für Admin-Prozesse mit Formularen und Tabellen.

Admin-Oberflächen richtig schneiden

Spring MVCThymeleafPRG PatternFlash Messages
Komplexes Beispiel 1: Admin-Controller mit Filterformular und Post/Redirect/Get

Verwendet ein Formularmodell und verhindert doppelte POSTs durch Redirect nach Aktion.

package com.seb4u.demo.spring;

@Controller
@RequestMapping("/admin/orders")
final class AdminOrderPageController {
    private final OrderQueryService orders;
    private final RecalculateOrderUseCase recalculate;

    AdminOrderPageController(OrderQueryService orders, RecalculateOrderUseCase recalculate) {
        this.orders = orders;
        this.recalculate = recalculate;
    }

    @GetMapping
    String list(@ModelAttribute OrderFilterForm filter, Model model) {
        var page = orders.search(filter.toSearch());
        model.addAttribute("orders", page.items());
        model.addAttribute("page", page);
        model.addAttribute("filter", filter);
        model.addAttribute("states", OrderState.values());
        return "admin/orders/list";
    }

    @PostMapping("/{id}/recalculate")
    String recalculate(@PathVariable String id, RedirectAttributes flash) {
        return switch (recalculate.handle(new OrderId(id))) {
            case RecalculateResult.Updated ok -> {
                flash.addFlashAttribute("success", "Order " + ok.id().value() + " recalculated");
                yield "redirect:/admin/orders/" + ok.id().value();
            }
            case RecalculateResult.NotFound nf -> {
                flash.addFlashAttribute("error", "Order not found");
                yield "redirect:/admin/orders";
            }
        };
    }
}

record OrderFilterForm(String customer, String state, int page, int size) {
    OrderSearch toSearch() {
        return new OrderSearch(
            Optional.ofNullable(customer).filter(s -> !s.isBlank()).map(CustomerId::new),
            Optional.ofNullable(state).filter(s -> !s.isBlank()).map(OrderState::valueOf),
            Math.max(0, page), size <= 0 ? 25 : Math.min(size, 100)
        );
    }
}
Komplexes Beispiel 2: Minimaler Thymeleaf-Template-Ausschnitt mit Fehler-/Success-Bereichen

Zeigt, wie Tabellen, Filter und Aktionen ohne API-Wildwuchs aufgebaut werden.

<!doctype html>
<html lang="de" xmlns:th="http://www.thymeleaf.org">
<body>
<section class="panel">
  <form method="get" th:object="${filter}">
    <input th:field="*{customer}" placeholder="Customer ID">
    <select th:field="*{state}">
      <option value="">Alle</option>
      <option th:each="s : ${states}" th:value="${s}" th:text="${s}"></option>
    </select>
    <button type="submit">Filtern</button>
  </form>

  <p class="success" th:if="${success}" th:text="${success}"></p>
  <p class="error" th:if="${error}" th:text="${error}"></p>

  <table>
    <tr><th>ID</th><th>Kunde</th><th>Status</th><th>Total</th><th>Aktion</th></tr>
    <tr th:each="order : ${orders}">
      <td th:text="${order.id}"></td>
      <td th:text="${order.customerId}"></td>
      <td th:text="${order.state}"></td>
      <td th:text="${order.total}"></td>
      <td>
        <form th:action="@{/admin/orders/{id}/recalculate(id=${order.id})}" method="post">
          <button type="submit">Neu berechnen</button>
        </form>
      </td>
    </tr>
  </table>
</section>
</body>
</html>
Finale Erweiterung

Frontend Integration mit JavaScript, React oder Vue

Modul H · Fullstack Java Edition · Alt-Referenz: K85

Clientseitige Oberflächen sauber an Java-Backends anbinden.

Frontend-Integration ohne Chaos

Fetch APIOptimistic UIProblemDetail HandlingContract Thinking

Auch ohne großes Framework muss Frontend-Code robuste Zustände behandeln: loading, success, validation error, conflict und network failure.

Komplexes Beispiel 1: JavaScript fetch mit Fehlervertrag und UI-State

Nutzt den REST-Vertrag aus Kapitel 83 und behandelt ProblemDetail-Antworten sauber.

const api = {
  async placeOrder(form) {
    const payload = {
      customerId: form.customerId.value.trim(),
      idempotencyKey: crypto.randomUUID(),
      lines: [...form.querySelectorAll("[data-line]")].map(row => ({
        sku: row.querySelector("[name=sku]").value.trim(),
        quantity: Number(row.querySelector("[name=quantity]").value),
        price: row.querySelector("[name=price]").value
      }))
    };

    const res = await fetch("/api/orders", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(payload)
    });

    if (!res.ok) {
      const problem = await res.json().catch(() => ({ title: "unknown", detail: res.statusText }));
      throw new Error(`${problem.title}: ${problem.detail}`);
    }
    return await res.json();
  }
};

document.querySelector("#order-form").addEventListener("submit", async event => {
  event.preventDefault();
  const form = event.currentTarget;
  const button = form.querySelector("button[type=submit]");
  const previous = button.textContent;
  button.disabled = true;
  button.textContent = "Speichere...";
  try {
    const order = await api.placeOrder(form);
    document.querySelector("#status").textContent = `Order ${order.id} wurde angelegt.`;
    form.reset();
  } catch (e) {
    document.querySelector("#status").textContent = e.message;
  } finally {
    button.disabled = false;
    button.textContent = previous;
  }
});
Komplexes Beispiel 2: TypeScript-artiger API-Client als Frontend-Grenze

Kapselt API-Aufrufe, damit UI-Komponenten nicht überall fetch-Details kennen.

class CommerceFlowClient {
  constructor(baseUrl) { this.baseUrl = baseUrl.replace(/\/$/, ""); }

  async request(path, options = {}) {
    const res = await fetch(this.baseUrl + path, {
      credentials: "same-origin",
      headers: { "content-type": "application/json", ...(options.headers || {}) },
      ...options
    });
    if (!res.ok) {
      const problem = await res.json().catch(() => ({ title: "HTTP " + res.status, detail: res.statusText }));
      throw Object.assign(new Error(problem.detail || problem.title), { problem, status: res.status });
    }
    return res.status === 204 ? null : res.json();
  }

  searchOrders({ page = 0, size = 25, customer = "" }) {
    const q = new URLSearchParams({ page, size, customer });
    return this.request(`/api/orders?${q}`);
  }

  placeOrder(command) {
    return this.request("/api/orders", { method: "POST", body: JSON.stringify(command) });
  }
}
Anti-Pattern: Jede Komponente ruft eigenständig beliebige URLs auf. Besser: ein kleiner API-Client mit gemeinsamen Fehlerregeln.
🔐
Finale Erweiterung

Login, Register und Rollen im Fullstack-Projekt

Modul H · Fullstack Java Edition · Alt-Referenz: K86

Authentifizierung und Autorisierung in UI, API und Use Cases konsistent umsetzen.

Login und Register Flow Login und Register Flow Register E-Mail, Passwort, Policy Hashing kein Klartext speichern Login Timing und generische Fehler Session/JWT Claims minimal halten Authorization Rollen am Use Case prüfen Audit kritische Events loggen Security ist kein Controller-Feature, sondern ein Querschnitt durch UI, API und Use Cases.

Auth-Flows produktionsnah denken

Password HashingSessions/JWTRolesAudit
Komplexes Beispiel 1: LoginService mit Dummy-Hash, Rollen und Token-Signer

Vermeidet User Enumeration und hält Token-Erzeugung außerhalb des Controllers.

package com.seb4u.demo.spring;

@Service
final class LoginService {
    private final UserRepository users;
    private final PasswordEncoder passwordEncoder;
    private final SessionTokenSigner signer;
    private final Clock clock;

    LoginService(UserRepository users, PasswordEncoder passwordEncoder, SessionTokenSigner signer, Clock clock) {
        this.users = users; this.passwordEncoder = passwordEncoder; this.signer = signer; this.clock = clock;
    }

    LoginOutcome login(LoginCommand command) {
        var user = users.findByEmail(command.email()).orElse(null);
        // Gleiches Timing-Fenster: auch bei unbekanntem User dummy hash prüfen.
        String hash = user == null ? PasswordEncoder.DUMMY_HASH : user.passwordHash();
        boolean passwordOk = passwordEncoder.matches(command.password(), hash);
        if (user == null || !passwordOk) return new LoginOutcome.Rejected("Invalid credentials");
        if (!user.enabled()) return new LoginOutcome.Rejected("Account disabled");

        var token = signer.sign(new SessionClaims(user.id(), user.roles(), clock.instant().plus(Duration.ofHours(8))));
        return new LoginOutcome.Accepted(token, user.roles());
    }
}

sealed interface LoginOutcome {
    record Accepted(String token, Set<Role> roles) implements LoginOutcome {}
    record Rejected(String reason) implements LoginOutcome {}
}
Komplexes Beispiel 2: Rollenprüfung am Use Case statt nur in der UI

Die UI darf Buttons verstecken, aber Sicherheit muss serverseitig im Use Case sitzen.

final class CancelOrderUseCase {
    private final OrderRepository orders;
    private final AuthorizationPolicy policy;

    CancelOrderUseCase(OrderRepository orders, AuthorizationPolicy policy) {
        this.orders = orders; this.policy = policy;
    }

    CancelOrderResult handle(UserPrincipal user, OrderId id) {
        Order order = orders.find(id).orElseThrow(() -> new NotFoundException("order"));
        if (!policy.canCancel(user, order)) return new CancelOrderResult.Forbidden();
        return order.cancel(user.id(), Instant.now())
            .map(updated -> { orders.save(updated); return new CancelOrderResult.Cancelled(updated.id()); })
            .orElseGet(reason -> new CancelOrderResult.Rejected(reason));
    }
}

final class AuthorizationPolicy {
    boolean canCancel(UserPrincipal user, Order order) {
        return user.roles().contains(Role.ADMIN) || order.customerId().equals(user.customerId());
    }
}

sealed interface CancelOrderResult {
    record Cancelled(OrderId id) implements CancelOrderResult {}
    record Forbidden() implements CancelOrderResult {}
    record Rejected(String reason) implements CancelOrderResult {}
}
📤
Finale Erweiterung

Datei-Upload, Export und Reporting

Modul H · Fullstack Java Edition · Alt-Referenz: K87

Fullstack-Anwendungen um Uploads, Downloads und große Exporte erweitern.

Datei-Upload und Export

Path Traversal SchutzQuarantineStreamingCSV
Komplexes Beispiel 1: Sicherer Upload mit Quarantine, Content-Type und SHA-256

Zeigt den produktionsnahen Umgang mit Dateinamen, Größenlimit und atomischem Verschieben.

final class SafeUploadService {
    private final Path uploadRoot;
    private final long maxBytes;
    private final Set<String> allowedContentTypes = Set.of("text/csv", "application/json");

    SafeUploadService(Path uploadRoot, long maxBytes) {
        this.uploadRoot = uploadRoot.toAbsolutePath().normalize();
        this.maxBytes = maxBytes;
    }

    StoredUpload store(String originalName, String contentType, InputStream in) throws IOException {
        if (!allowedContentTypes.contains(contentType)) {
            throw new IllegalArgumentException("Unsupported content type: " + contentType);
        }
        String safeName = sanitize(originalName);
        Path quarantine = uploadRoot.resolve("quarantine").resolve(UUID.randomUUID() + "-" + safeName);
        Files.createDirectories(quarantine.getParent());

        try (var limited = new BoundedInputStream(in, maxBytes);
             var out = Files.newOutputStream(quarantine, StandardOpenOption.CREATE_NEW)) {
            limited.transferTo(out);
        }

        if (Files.size(quarantine) == 0 || Files.size(quarantine) > maxBytes) {
            Files.deleteIfExists(quarantine);
            throw new IOException("Upload size invalid");
        }

        String sha256 = sha256(quarantine);
        Path finalPath = uploadRoot.resolve("accepted").resolve(sha256 + "-" + safeName).normalize();
        if (!finalPath.startsWith(uploadRoot.resolve("accepted"))) throw new SecurityException("Path traversal");
        Files.createDirectories(finalPath.getParent());
        Files.move(quarantine, finalPath, StandardCopyOption.ATOMIC_MOVE);
        return new StoredUpload(finalPath.getFileName().toString(), sha256, Files.size(finalPath));
    }

    private static String sanitize(String name) {
        return Path.of(name).getFileName().toString().replaceAll("[^a-zA-Z0-9._-]", "_");
    }

    private static String sha256(Path path) throws IOException {
        try {
            var digest = MessageDigest.getInstance("SHA-256");
            try (var in = Files.newInputStream(path)) {
                byte[] buf = new byte[8192];
                for (int read; (read = in.read(buf)) != -1;) digest.update(buf, 0, read);
            }
            return HexFormat.of().formatHex(digest.digest());
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException(e);
        }
    }
}

record StoredUpload(String fileName, String sha256, long bytes) {}
Komplexes Beispiel 2: CSV-Export als Streaming Response

Exportiert große Datenmengen ohne komplette Liste im Speicher.

package com.seb4u.demo.spring;

@RestController
@RequestMapping("/api/exports")
final class ExportController {
    private final OrderQueryService orders;

    ExportController(OrderQueryService orders) { this.orders = orders; }

    @GetMapping(value = "/orders.csv", produces = "text/csv")
    void exportOrders(HttpServletResponse response) throws IOException {
        response.setHeader("content-disposition", "attachment; filename=orders.csv");
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());

        try (var writer = response.getWriter()) {
            writer.println("id,customer,state,total,createdAt");
            orders.streamAll().forEach(order -> {
                writer.printf("%s,%s,%s,%s,%s%n",
                    csv(order.id()), csv(order.customerId()), csv(order.state().name()),
                    csv(order.total().toPlainString()), csv(order.createdAt().toString()));
                if (writer.checkError()) throw new UncheckedIOException(new IOException("client disconnected"));
            });
        } catch (UncheckedIOException e) {
            throw e.getCause();
        }
    }

    private static String csv(String value) {
        String escaped = value.replace("\"", "\"\"");
        return '"' + escaped + '"';
    }
}
Master-Regel: Uploads sind untrusted input. Dateiname, Pfad, Content-Type und Größe müssen aktiv kontrolliert werden.
Finale Erweiterung

Fullstack Testing, UX und Fehlerseiten

Modul H · Fullstack Java Edition · Alt-Referenz: K88

Webflows testbar, nutzerfreundlich und supportfähig machen.

ThemaProfessionelle LösungWarum
FormularvalidierungClientseitig für UX, serverseitig verbindlichBrowser-Regeln sind leicht umgehbar.
Fehlerseiten404, 403, 500 mit Correlation-IDSupport kann Fehler nachvollziehen.
PaginationWhitelist-Sorting und Max-Page-SizeSchützt Datenbank und API.
UI TestsHappy Path plus FehlerfälleRegressionen in kritischen Flows verhindern.

Fullstack Test- und UX-Regeln

MockMvcHTML AssertionsProblemDetailAccessibility
Komplexes Beispiel 1: MockMvc-Test für JSON API und Problem Details

Prüft nicht nur Statuscodes, sondern den stabilen Fehlervertrag.

package com.seb4u.demo.spring;

@WebMvcTest(OrderRestController.class)
class OrderRestControllerTest {
    @Autowired MockMvc mvc;
    @MockitoBean PlaceOrderUseCase placeOrder;
    @MockitoBean OrderQueryService queries;

    @Test
    void rejectsInvalidOrderWithProblemDetail() throws Exception {
        mvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"customerId":"","idempotencyKey":"abc","lines":[]}
                    """))
            .andExpect(status().isBadRequest())
            .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_PROBLEM_JSON));
    }
}
Komplexes Beispiel 2: UI Smoke Test für Admin-Seite

Prüft, dass Tabelle, Filterformular und Aktionsbutton im gerenderten HTML existieren.

package com.seb4u.demo.spring;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class AdminOrderPageSmokeTest {
    @Autowired TestRestTemplate http;

    @Test
    void adminOrdersPageContainsFilterAndTable() {
        ResponseEntity<String> res = http.getForEntity("/admin/orders", String.class);
        assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(res.getBody())
            .contains("<form")
            .contains("name=\"customer\"")
            .contains("<table")
            .contains("Neu berechnen");
    }
}
Modul I

☁️ Microservices, Messaging & Cloud

Gateway, Service Discovery, Kafka/RabbitMQ, Outbox, Sagas, Resilience, Docker, Kubernetes, Observability und CI/CD.

8Themen in diesem Modul
13Codeblöcke
3SVG-Grafiken
☁️
Finale Erweiterung

Microservices und Cloud Edition Überblick

Modul I · Microservices, Messaging & Cloud · Alt-Referenz: K89

Verteilte Systeme realistisch verstehen: Nutzen, Kosten, Grenzen und Betriebsanforderungen.

Microservices und Cloud-Landkarte Microservices und Cloud-Landkarte API Gateway Routing, Auth, Limits Services Bounded Contexts Messaging Kafka/RabbitMQ Events Data Ownership DB pro Service Resilience Retries, Circuit Breaker Platform Docker, Kubernetes, Observability Microservices lösen Organisations- und Skalierungsprobleme, erzeugen aber verteilte Komplexität.

Monolith zuerst

Ein modularer Monolith ist oft die bessere Startarchitektur. Microservices brauchen Reife in Betrieb und Teams.

Klare Grenzen

Jeder Service besitzt seine Daten und veröffentlicht Ereignisse statt fremde Tabellen zu lesen.

Fehler sind normal

Timeouts, Retries, Idempotenz und Dead-letter sind Kernbestandteile, nicht Extras.

Observability

Ohne Logs, Metrics und Traces werden verteilte Systeme blind.

FrageMonolithMicroservices
Teamgrößeklein/mittelmehrere Teams mit klaren Domains
Deploymentein Artefaktviele Artefakte und Pipelines
Datenmodellgemeinsame DB möglichDB ownership pro Service
Fehlerbildlokal debugbarNetzwerk, Latenz, Teilfehler
🚪
Finale Erweiterung

API Gateway, Service Discovery und Konfiguration

Modul I · Microservices, Messaging & Cloud · Alt-Referenz: K90

Randlogik, Routing und serviceübergreifende Header professionell schneiden.

Gateway und Service-Grenzen

API GatewayCorrelation IDTenant RoutingService Contracts
Komplexes Beispiel 1: Gateway-Filter mit Tenant- und Correlation-Headern

Zeigt eine typische Querschnittsfunktion am Rand des Systems.

@Component
final class CorrelationAndTenantGatewayFilter implements GlobalFilter, Ordered {
    private static final Pattern TENANT = Pattern.compile("[a-z0-9-]{3,40}");

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String correlationId = Optional.ofNullable(exchange.getRequest().getHeaders().getFirst("x-correlation-id"))
            .filter(id -> id.length() <= 80)
            .orElse(UUID.randomUUID().toString());

        String tenant = exchange.getRequest().getHeaders().getFirst("x-tenant");
        if (tenant == null || !TENANT.matcher(tenant).matches()) {
            exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
            return exchange.getResponse().setComplete();
        }

        var mutated = exchange.mutate().request(builder -> builder
            .header("x-correlation-id", correlationId)
            .header("x-tenant", tenant)
            .header("x-forwarded-at", Instant.now().toString())
        ).build();

        return chain.filter(mutated)
            .contextWrite(ctx -> ctx.put("correlationId", correlationId).put("tenant", tenant));
    }

    @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE + 10; }
}
Komplexes Beispiel 2: Service Contract als explizites Java-Interface

Services kommunizieren über stabile Verträge, nicht über geteilte interne Klassen.

public interface InventoryApi {
    ReservationResponse reserve(ReservationRequest request);
    void release(ReservationId reservationId);
}

public record ReservationRequest(String orderId, List<Item> items, String correlationId) {
    public ReservationRequest {
        if (items == null || items.isEmpty()) throw new IllegalArgumentException("items required");
    }
}

public sealed interface ReservationResponse {
    record Reserved(String reservationId, Instant expiresAt) implements ReservationResponse {}
    record Rejected(String reason) implements ReservationResponse {}
}

final class InventoryClientAdapter implements InventoryApi {
    private final HttpClient client;
    private final URI baseUri;
    private final JsonCodec json;

    public ReservationResponse reserve(ReservationRequest request) {
        HttpRequest http = HttpRequest.newBuilder(baseUri.resolve("/reservations"))
            .timeout(Duration.ofSeconds(2))
            .header("content-type", "application/json")
            .header("x-correlation-id", request.correlationId())
            .POST(HttpRequest.BodyPublishers.ofString(json.encode(request)))
            .build();
        return json.decode(send(http), ReservationResponse.class);
    }
}
📨
Finale Erweiterung

Messaging, Kafka/RabbitMQ und Outbox Pattern

Modul I · Microservices, Messaging & Cloud · Alt-Referenz: K91

Asynchrone Kommunikation robust, idempotent und nachvollziehbar gestalten.

Outbox und Messaging Flow Outbox und Messaging Flow DB Transaction Business State + Outbox Row Relay Batch lock + publish Broker Topic/Queue Consumer Idempotent handler Projection Read Model aktualisieren Dead-letter nicht verlierbare Fehler Outbox verbindet lokale Transaktion mit asynchroner Kommunikation ohne verteilte DB-Transaktion.

Messaging ohne Datenverlust

OutboxKafka/RabbitMQAt-least-onceIdempotenz
Komplexes Beispiel 1: Outbox Relay mit Backoff und Dead-letter

Ein realistisches Grundmuster für Event-Publishing nach erfolgreicher Datenbanktransaktion.

final class OutboxRelay implements Runnable {
    private final OutboxRepository outbox;
    private final EventPublisher publisher;
    private final Duration idleDelay;

    OutboxRelay(OutboxRepository outbox, EventPublisher publisher, Duration idleDelay) {
        this.outbox = outbox; this.publisher = publisher; this.idleDelay = idleDelay;
    }

    @Override public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            var batch = outbox.lockNextBatch(100); // SQL: for update skip locked
            if (batch.isEmpty()) {
                LockSupport.parkNanos(idleDelay.toNanos());
                continue;
            }
            for (OutboxMessage msg : batch) {
                try {
                    publisher.publish(msg.topic(), msg.key(), msg.payload(), msg.headers());
                    outbox.markPublished(msg.id(), Instant.now());
                } catch (TransientPublisherException e) {
                    outbox.releaseWithBackoff(msg.id(), nextRetry(msg.attempts()));
                } catch (Exception fatal) {
                    outbox.moveToDeadLetter(msg.id(), fatal.getMessage());
                }
            }
        }
    }

    private static Instant nextRetry(int attempts) {
        long seconds = Math.min(300, (long) Math.pow(2, attempts));
        return Instant.now().plusSeconds(seconds);
    }
}
Komplexes Beispiel 2: Idempotenter Consumer mit deduplication table

At-least-once Delivery bedeutet: Consumer müssen Duplikate vertragen.

final class OrderEventConsumer {
    private final ProcessedMessageStore processed;
    private final OrderProjection projection;

    void onMessage(MessageEnvelope envelope) {
        if (!processed.tryMarkProcessing(envelope.messageId())) return; // Duplikat
        try {
            switch (envelope.event()) {
                case OrderPlaced e -> projection.insert(e.orderId(), e.customerId(), e.total());
                case OrderCancelled e -> projection.markCancelled(e.orderId(), e.reason());
                case OrderShipped e -> projection.markShipped(e.orderId(), e.trackingId());
            }
            processed.markDone(envelope.messageId());
        } catch (Exception e) {
            processed.markFailed(envelope.messageId(), e.getMessage());
            throw e;
        }
    }
}

record MessageEnvelope(UUID messageId, DomainEvent event) {}
sealed interface DomainEvent permits OrderPlaced, OrderCancelled, OrderShipped {}
record OrderPlaced(String orderId, String customerId, BigDecimal total) implements DomainEvent {}
record OrderCancelled(String orderId, String reason) implements DomainEvent {}
record OrderShipped(String orderId, String trackingId) implements DomainEvent {}
🔁
Finale Erweiterung

Verteilte Transaktionen, Saga und Idempotenz

Modul I · Microservices, Messaging & Cloud · Alt-Referenz: K92

Konsistenz in verteilten Systemen ohne globale Datenbanktransaktion modellieren.

Saga statt verteilter Transaktion

SagaCompensationIdempotencyTimeouts
Komplexes Beispiel 1: Order Saga mit Kompensationsschritten

Zeigt ein orchestriertes Saga-Grundmuster für Inventory, Payment und Shipping.

final class OrderSagaCoordinator {
    private final InventoryClient inventory;
    private final PaymentClient payment;
    private final ShippingClient shipping;
    private final SagaLog log;

    SagaResult execute(OrderId orderId, Money amount) {
        var steps = new ArrayList<Compensation>();
        try {
            var reservation = inventory.reserve(orderId);
            steps.add(() -> inventory.release(reservation));

            var charge = payment.authorize(orderId, amount);
            steps.add(() -> payment.voidAuthorization(charge));

            var shipment = shipping.createLabel(orderId);
            log.completed(orderId, List.of("inventory", "payment", "shipping"));
            return new SagaResult.Completed(shipment.trackingId());
        } catch (Exception failure) {
            Collections.reverse(steps);
            for (Compensation compensation : steps) {
                try { compensation.run(); }
                catch (Exception compensationFailure) { log.compensationFailed(orderId, compensationFailure); }
            }
            return new SagaResult.Failed(failure.getMessage());
        }
    }

    @FunctionalInterface interface Compensation { void run(); }
}

sealed interface SagaResult {
    record Completed(String trackingId) implements SagaResult {}
    record Failed(String reason) implements SagaResult {}
}
Komplexes Beispiel 2: Idempotency-Service um Use Cases herum

Verhindert doppelte Ausführung bei Client-Retry oder Gateway-Timeout.

final class IdempotencyService {
    private final IdempotencyStore store;
    private final Clock clock;

    <T> T execute(IdempotencyKey key, Supplier<T> work, Function<T, byte[]> serialize, Function<byte[], T> deserialize) {
        return switch (store.tryStart(key, clock.instant())) {
            case IdempotencyDecision.Replay replay -> deserialize.apply(replay.responseBody());
            case IdempotencyDecision.InProgress inProgress ->
                throw new ConflictException("Request is already running: " + key.value());
            case IdempotencyDecision.Started started -> {
                try {
                    T result = work.get();
                    store.complete(key, serialize.apply(result), clock.instant());
                    yield result;
                } catch (RuntimeException e) {
                    store.fail(key, e.getMessage(), clock.instant());
                    throw e;
                }
            }
        };
    }
}

sealed interface IdempotencyDecision {
    record Started() implements IdempotencyDecision {}
    record InProgress(Instant startedAt) implements IdempotencyDecision {}
    record Replay(byte[] responseBody) implements IdempotencyDecision {}
}
Senior-Hinweis: Eine Saga garantiert nicht, dass nie Zwischenzustände sichtbar sind. Sie garantiert kontrollierte Vorwärts- oder Kompensationslogik.
🛡️
Finale Erweiterung

Resilience: Circuit Breaker, Rate Limiting und Bulkheads

Modul I · Microservices, Messaging & Cloud · Alt-Referenz: K93

Verteilte Fehler kontrollieren, statt sie durch das ganze System laufen zu lassen.

Resilience-Bausteine

Circuit BreakerRate LimitBulkheadTimeout
Komplexes Beispiel 1: Circuit Breaker mit Closed/Open/Half-Open States

Zeigt das Zustandsmodell hinter Resilience-Libraries und macht Fehlerschwellen explizit.

final class CircuitBreaker {
    private final int failureThreshold;
    private final Duration openFor;
    private final AtomicReference<State> state = new AtomicReference<>(new Closed(0));
    private final Clock clock;

    CircuitBreaker(int failureThreshold, Duration openFor, Clock clock) {
        this.failureThreshold = failureThreshold; this.openFor = openFor; this.clock = clock;
    }

    <T> T call(CheckedSupplier<T> supplier) throws Exception {
        State current = state.get();
        if (current instanceof Open open && clock.instant().isBefore(open.retryAfter())) {
            throw new ServiceUnavailableException("circuit open until " + open.retryAfter());
        }
        if (current instanceof Open open && clock.instant().isAfter(open.retryAfter())) {
            state.compareAndSet(current, new HalfOpen());
        }

        try {
            T result = supplier.get();
            state.set(new Closed(0));
            return result;
        } catch (Exception e) {
            state.updateAndGet(s -> switch (s) {
                case Closed c when c.failures() + 1 >= failureThreshold -> new Open(clock.instant().plus(openFor));
                case Closed c -> new Closed(c.failures() + 1);
                case HalfOpen h -> new Open(clock.instant().plus(openFor));
                case Open o -> o;
            });
            throw e;
        }
    }

    sealed interface State permits Closed, Open, HalfOpen {}
    record Closed(int failures) implements State {}
    record Open(Instant retryAfter) implements State {}
    record HalfOpen() implements State {}
    @FunctionalInterface interface CheckedSupplier<T> { T get() throws Exception; }
}
Komplexes Beispiel 2: Token-Bucket Rate Limiter

Ein Lernbeispiel für kontrollierten Durchsatz und Schutz vor Lastspitzen.

final class TokenBucketRateLimiter {
    private final long capacity;
    private final long refillPerSecond;
    private long tokens;
    private long lastRefillNanos;

    TokenBucketRateLimiter(long capacity, long refillPerSecond, ClockSource clock) {
        this.capacity = capacity; this.refillPerSecond = refillPerSecond;
        this.tokens = capacity; this.lastRefillNanos = clock.nanoTime();
    }

    synchronized boolean tryAcquire(int permits, ClockSource clock) {
        refill(clock.nanoTime());
        if (permits <= tokens) {
            tokens -= permits;
            return true;
        }
        return false;
    }

    private void refill(long nowNanos) {
        long elapsed = nowNanos - lastRefillNanos;
        long toAdd = (elapsed * refillPerSecond) / 1_000_000_000L;
        if (toAdd > 0) {
            tokens = Math.min(capacity, tokens + toAdd);
            lastRefillNanos = nowNanos;
        }
    }

    interface ClockSource { long nanoTime(); }
}
ProblemPatternWichtig
Langsamer DownstreamTimeout + Circuit BreakerNie unendlich warten.
ÜberlastungRate Limiting + BulkheadFehler isolieren statt global eskalieren.
NetzwerkfehlerRetry mit BackoffNur idempotente Operationen automatisch wiederholen.
Dauerhafte FehlerDead-letter + AlertNicht endlos im Kreis verarbeiten.
🐳
Finale Erweiterung

Docker Compose und Kubernetes Grundlagen

Modul I · Microservices, Messaging & Cloud · Alt-Referenz: K94

Java-Services lokal und im Cluster betreibbar machen.

Lokale Cloud-Umgebung

Docker ComposePostgreSQLKafkaHealth Checks
Komplexes Beispiel 1: docker-compose für API, PostgreSQL und Kafka

Lokale Entwicklungsumgebung für CommerceFlow mit Healthcheck und Volumes.

services:
  commerceflow-api:
    build: .
    environment:
      SPRING_PROFILES_ACTIVE: docker
      DB_URL: jdbc:postgresql://postgres:5432/commerceflow
      KAFKA_BOOTSTRAP_SERVERS: kafka:9092
    ports: ["8080:8080"]
    depends_on:
      postgres:
        condition: service_healthy
      kafka:
        condition: service_started

  postgres:
    image: postgres:17
    environment:
      POSTGRES_DB: commerceflow
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d commerceflow"]
      interval: 5s
      timeout: 3s
      retries: 10
    volumes:
      - pgdata:/var/lib/postgresql/data

  kafka:
    image: apache/kafka:4.0.0
    ports: ["9092:9092"]

volumes:
  pgdata:
Komplexes Beispiel 2: Kubernetes Deployment und Service

Grundstruktur für Deployment, Probes, Ressourcen und Service-Exposure.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: commerceflow-api
spec:
  replicas: 3
  selector:
    matchLabels: { app: commerceflow-api }
  template:
    metadata:
      labels: { app: commerceflow-api }
    spec:
      containers:
        - name: app
          image: registry.example.com/commerceflow-api:1.0.0
          ports: [{ containerPort: 8080 }]
          envFrom:
            - configMapRef: { name: commerceflow-config }
            - secretRef: { name: commerceflow-secrets }
          readinessProbe:
            httpGet: { path: /actuator/health/readiness, port: 8080 }
            periodSeconds: 10
          livenessProbe:
            httpGet: { path: /actuator/health/liveness, port: 8080 }
            periodSeconds: 20
          resources:
            requests: { cpu: "250m", memory: "512Mi" }
            limits: { cpu: "1000m", memory: "1Gi" }
---
apiVersion: v1
kind: Service
metadata:
  name: commerceflow-api
spec:
  selector: { app: commerceflow-api }
  ports:
    - port: 80
      targetPort: 8080
Master-Regel: Containerisierung ist nicht nur ein Dockerfile. Dazu gehören Konfiguration, Healthchecks, Ressourcenlimits und reproduzierbare Startreihenfolge.
📈
Finale Erweiterung

Cloud Observability: Logs, Metrics, Traces

Modul I · Microservices, Messaging & Cloud · Alt-Referenz: K95

Produktionsprobleme sichtbar und diagnostizierbar machen.

Observability Pipeline Observability Pipeline Application structured logs Metrics latency, errors, saturation Traces request across services Dashboards SLO visibility Alerts actionable signals Runbooks known fixes Observability beantwortet: Was passiert, warum passiert es und was sollen wir tun?

Logs, Metrics und Traces

Correlation IDMDCHTTP LogsRunbooks
Komplexes Beispiel 1: RequestLogFilter mit Correlation-ID und strukturierten Logs

Macht Support und Traceability in verteilten Systemen möglich.

final class RequestLogFilter extends OncePerRequestFilter {
    private static final Logger log = LoggerFactory.getLogger(RequestLogFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws ServletException, IOException {
        long start = System.nanoTime();
        String correlationId = Optional.ofNullable(req.getHeader("x-correlation-id"))
            .orElse(UUID.randomUUID().toString());
        MDC.put("correlationId", correlationId);
        MDC.put("method", req.getMethod());
        MDC.put("path", req.getRequestURI());
        try {
            chain.doFilter(req, res);
        } finally {
            long millis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
            log.info("http_request method={} path={} status={} durationMs={} correlationId={}",
                req.getMethod(), req.getRequestURI(), res.getStatus(), millis, correlationId);
            MDC.clear();
        }
    }
}
Komplexes Beispiel 2: Minimaler Health Indicator für Downstream-Abhängigkeit

Health Checks müssen zwischen App lebt und App ist bereit unterscheiden.

@Component
final class InventoryHealthIndicator implements HealthIndicator {
    private final InventoryClient inventory;

    InventoryHealthIndicator(InventoryClient inventory) { this.inventory = inventory; }

    @Override
    public Health health() {
        try {
            Duration latency = inventory.ping();
            return Health.up()
                .withDetail("latencyMs", latency.toMillis())
                .withDetail("dependency", "inventory")
                .build();
        } catch (Exception e) {
            return Health.down(e)
                .withDetail("dependency", "inventory")
                .withDetail("action", "check inventory service or network policy")
                .build();
        }
    }
}
🚀
Finale Erweiterung

Cloud Release Checklist und CI/CD

Modul I · Microservices, Messaging & Cloud · Alt-Referenz: K96

Von lokalem Build zu sicherem Release-Prozess.

ArtefaktMuss enthaltenPrüffrage
Dockerfilenon-root user, layers, healthKann das Image reproduzierbar gebaut werden?
ComposeDB, Broker, HealthchecksKann ein Entwickler lokal alles starten?
KubernetesDeployment, Service, Probes, ResourcesKann der Cluster sauber rollen und heilen?
CI/CDTests, Scan, Build, Release NotesWird jedes Release nachvollziehbar ausgeliefert?

Release Pipeline als Checkliste

  • Build ist reproduzierbar und pinnt Plugin-/Dependency-Versionen.
  • Unit-, Integration- und Architekturtests laufen getrennt.
  • Secrets kommen nicht ins Image und nicht ins Repository.
  • Migrationen sind rückwärtskompatibel oder haben klaren Rollout-Plan.
  • Monitoring-Dashboards und Alerts existieren vor dem Go-live.
  • Rollback oder Roll-forward ist dokumentiert.
Komplexes Beispiel: GitHub Actions Pipeline für Java Service

Build, Test, Container Build und Artefakt-Upload als realistische CI-Grundlage.

name: commerceflow-ci
on: [push, pull_request]
jobs:
  build-test-package:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '25'
          cache: gradle
      - run: ./gradlew clean test integrationTest jacocoTestReport
      - run: ./gradlew bootBuildImage --imageName ghcr.io/acme/commerceflow:${{ github.sha }}
      - name: Upload reports
        uses: actions/upload-artifact@v4
        with:
          name: test-reports
          path: build/reports
Modul J

🧮 Algorithmen & Datenstruktur Mastery

Big-O, Collections-Auswahl, HashMap, Sliding Window, Graphen, DP, Backtracking, Trie und Interview-Patterns.

7Themen in diesem Modul
13Codeblöcke
1SVG-Grafiken
🧠
Finale Erweiterung

Algorithmus und Datenstruktur Mastery Überblick

Modul J · Algorithmen & Datenstruktur Mastery · Alt-Referenz: K97

Algorithmisches Denken mit Java-Implementierungen und Interview-Erklärungen verbinden.

Algorithmus-Mastery Landkarte Algorithmus-Mastery Landkarte Big-O Zeit und Speicher Arrays/Strings Sliding Window, Two Pointers Hashing Maps, Sets, Frequency Graphs BFS, DFS, Dijkstra DP Zustand, Übergang, Tabelle Design Trade-offs erklären Algorithmus-Mastery bedeutet: Lösung, Komplexität und Edge Cases sauber begründen.

Denkmuster

Problemtyp erkennen: Suche, Sortierung, Pfad, Optimierung, Mengen, Sequenzen.

Komplexität

Immer Zeit und Speicher nennen, inklusive Worst Case und Datenstrukturkosten.

Java-Fit

Die richtige Collection wählen: ArrayDeque, HashMap, PriorityQueue, TreeMap, BitSet.

Interview-Fit

Lösung laut strukturieren: Brute Force, Verbesserung, Proof, Edge Cases.

📏
Finale Erweiterung

Big-O, Benchmark-Denken und Collection-Auswahl

Modul J · Algorithmen & Datenstruktur Mastery · Alt-Referenz: K98

Komplexität nicht nur auswendig lernen, sondern messen und begründen.

Big-O sichtbar machen

Benchmark-DenkenO(n)O(n²)Messfallen
Komplexes Beispiel 1: Mini Complexity Probe für lineare und quadratische Laufzeit

Kein Ersatz für JMH, aber sehr gut, um Wachstumsraten praktisch zu sehen.

final class ComplexityProbe {
    static long measureNanos(String name, int n, IntConsumer algorithm) {
        // Warmup: kein echter JMH-Ersatz, aber gut für Lernexperimente.
        for (int i = 0; i < 3; i++) algorithm.accept(Math.max(1, n / 10));
        long start = System.nanoTime();
        algorithm.accept(n);
        long took = System.nanoTime() - start;
        System.out.printf("%-20s n=%8d took=%8.3f ms%n", name, n, took / 1_000_000.0);
        return took;
    }

    public static void main(String[] args) {
        for (int n : List.of(1_000, 2_000, 4_000, 8_000, 16_000)) {
            measureNanos("linear", n, ComplexityProbe::linear);
            measureNanos("quadratic", n, ComplexityProbe::quadratic);
        }
    }

    static void linear(int n) {
        long sum = 0;
        for (int i = 0; i < n; i++) sum += i;
        Blackhole.consume(sum);
    }

    static void quadratic(int n) {
        long sum = 0;
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                sum += i ^ j;
        Blackhole.consume(sum);
    }
}

final class Blackhole {
    static volatile long sink;
    static void consume(long v) { sink = v; }
}
Komplexes Beispiel 2: Auswahlregeln für Collections als Code-Kommentar-Template

Hilft, Designentscheidungen in Code Reviews nachvollziehbar zu machen.

/*
Collection Decision Template
- Zugriff per Index?        ArrayList
- Viele FIFO/LIFO Operationen? ArrayDeque
- Eindeutigkeit + O(1)?     HashSet
- Sortierte Iteration?      TreeSet / TreeMap
- Priorität statt Sortierung? PriorityQueue
- Häufigkeitszählung?       HashMap<T, Integer>
- Sehr viele Booleans?      BitSet

Immer prüfen:
- Was ist die dominante Operation?
- Wie groß ist n realistisch?
- Ist Reihenfolge wichtig?
- Ist Thread-Sicherheit erforderlich?
- Gibt es Speichergrenzen?
*/
🗂️
Finale Erweiterung

HashMap, Sets und Frequency Patterns

Modul J · Algorithmen & Datenstruktur Mastery · Alt-Referenz: K99

Hash-basierte Datenstrukturen intern verstehen und in Aufgaben sicher anwenden.

Hashing und Maps verstehen

HashMapLoad FactorCollisionResize
Komplexes Beispiel 1: TinyHashMap mit Buckets und Resize

Lernimplementierung, um Hashing, Kollisionen und Rehashing zu verstehen.

final class TinyHashMap<K, V> {
    private static final int DEFAULT_CAPACITY = 16;
    private Entry<K, V>[] table = new Entry[DEFAULT_CAPACITY];
    private int size;

    V put(K key, V value) {
        resizeIfNeeded();
        int idx = index(key, table.length);
        for (Entry<K,V> e = table[idx]; e != null; e = e.next) {
            if (Objects.equals(e.key, key)) {
                V old = e.value;
                e.value = value;
                return old;
            }
        }
        table[idx] = new Entry<>(key, value, table[idx]);
        size++;
        return null;
    }

    Optional<V> get(K key) {
        int idx = index(key, table.length);
        for (Entry<K,V> e = table[idx]; e != null; e = e.next) {
            if (Objects.equals(e.key, key)) return Optional.ofNullable(e.value);
        }
        return Optional.empty();
    }

    private void resizeIfNeeded() {
        if (size < table.length * 0.75) return;
        Entry<K,V>[] old = table;
        table = new Entry[old.length * 2];
        size = 0;
        for (Entry<K,V> bucket : old)
            for (Entry<K,V> e = bucket; e != null; e = e.next)
                put(e.key, e.value);
    }

    private int index(K key, int capacity) {
        int h = key == null ? 0 : key.hashCode();
        h ^= (h >>> 16); // einfache Streuung wie Grundidee hinter HashMap
        return h & (capacity - 1);
    }

    record Entry<K,V>(K key, V value, Entry<K,V> next) {}
}
Komplexes Beispiel 2: Frequency Map für Top-K Wörter

Typische Interview- und Produktionsaufgabe mit HashMap + PriorityQueue.

final class TopKWords {
    List<String> topK(List<String> words, int k) {
        Map<String, Integer> freq = new HashMap<>();
        for (String word : words) freq.merge(normalize(word), 1, Integer::sum);

        PriorityQueue<Map.Entry<String, Integer>> heap = new PriorityQueue<>(
            Comparator.<Map.Entry<String, Integer>>comparingInt(Map.Entry::getValue)
                .thenComparing(Map.Entry::getKey, Comparator.reverseOrder())
        );
        for (var e : freq.entrySet()) {
            heap.offer(e);
            if (heap.size() > k) heap.poll();
        }
        var result = new ArrayList<String>();
        while (!heap.isEmpty()) result.add(heap.poll().getKey());
        Collections.reverse(result);
        return result;
    }

    private String normalize(String word) { return word.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]", ""); }
}
🧵
Finale Erweiterung

Arrays, Strings, Sliding Window und Two Pointers

Modul J · Algorithmen & Datenstruktur Mastery · Alt-Referenz: K100

Sequenzaufgaben effizient lösen und Edge Cases sauber behandeln.

Strings und Fenstertechniken

Sliding WindowTwo PointersUnicodeEdge Cases
Komplexes Beispiel 1: Longest Substring ohne Wiederholung mit Unicode Code Points

Fortgeschrittener als char-basierte Lösungen, weil Unicode korrekt berücksichtigt wird.

final class LongestSubstringWithoutRepeatingChars {
    int lengthOfLongestSubstring(String s) {
        Map<Integer, Integer> lastSeen = new HashMap<>();
        int left = 0;
        int best = 0;
        int[] codePoints = s.codePoints().toArray();

        for (int right = 0; right < codePoints.length; right++) {
            int cp = codePoints[right];
            Integer previous = lastSeen.put(cp, right);
            if (previous != null && previous >= left) {
                left = previous + 1;
            }
            best = Math.max(best, right - left + 1);
        }
        return best;
    }
}
Komplexes Beispiel 2: Two-Pointer Merge von Intervallen

Klassisches Muster für sortierte Bereiche, Kalender und Reservierungen.

final class IntervalMerger {
    List<Interval> merge(List<Interval> intervals) {
        if (intervals.isEmpty()) return List.of();
        var sorted = intervals.stream().sorted(Comparator.comparing(Interval::start)).toList();
        var out = new ArrayList<Interval>();
        Interval current = sorted.getFirst();
        for (int i = 1; i < sorted.size(); i++) {
            Interval next = sorted.get(i);
            if (next.start() <= current.end()) {
                current = new Interval(current.start(), Math.max(current.end(), next.end()));
            } else {
                out.add(current);
                current = next;
            }
        }
        out.add(current);
        return out;
    }
}

record Interval(int start, int end) {
    Interval { if (end < start) throw new IllegalArgumentException("end < start"); }
}
🕸️
Finale Erweiterung

Graphen: BFS, DFS, Dijkstra, Union-Find

Modul J · Algorithmen & Datenstruktur Mastery · Alt-Referenz: K101

Graphprobleme erkennen, modellieren und mit Java-Collections lösen.

Graphen in Java

BFS/DFSDijkstraUnion-FindTopological Sort
Komplexes Beispiel 1: Dijkstra mit PriorityQueue und veralteten Queue-Einträgen

Standardmuster für nicht-negative Kantengewichte.

final class Dijkstra {
    Map<Node, Integer> shortestPaths(Graph graph, Node start) {
        var dist = new HashMap<Node, Integer>();
        var pq = new PriorityQueue<NodeDistance>(Comparator.comparingInt(NodeDistance::distance));
        dist.put(start, 0);
        pq.add(new NodeDistance(start, 0));

        while (!pq.isEmpty()) {
            var current = pq.poll();
            if (current.distance() != dist.getOrDefault(current.node(), Integer.MAX_VALUE)) continue;

            for (Edge edge : graph.edgesFrom(current.node())) {
                int next = Math.addExact(current.distance(), edge.weight());
                if (next < dist.getOrDefault(edge.to(), Integer.MAX_VALUE)) {
                    dist.put(edge.to(), next);
                    pq.add(new NodeDistance(edge.to(), next));
                }
            }
        }
        return dist;
    }
}

record Node(String id) {}
record Edge(Node to, int weight) { Edge { if (weight < 0) throw new IllegalArgumentException("Dijkstra needs non-negative weights"); } }
record NodeDistance(Node node, int distance) {}
interface Graph { List<Edge> edgesFrom(Node node); }
Komplexes Beispiel 2: Union-Find mit Path Compression und Union by Rank

Sehr wichtig für Connectivity, Kruskal und Komponentenprobleme.

final class UnionFind<T> {
    private final Map<T, T> parent = new HashMap<>();
    private final Map<T, Integer> rank = new HashMap<>();

    void add(T item) {
        parent.putIfAbsent(item, item);
        rank.putIfAbsent(item, 0);
    }

    T find(T item) {
        add(item);
        T p = parent.get(item);
        if (!p.equals(item)) {
            parent.put(item, find(p)); // Pfadkompression
        }
        return parent.get(item);
    }

    boolean union(T a, T b) {
        T rootA = find(a);
        T rootB = find(b);
        if (rootA.equals(rootB)) return false;

        int rankA = rank.get(rootA);
        int rankB = rank.get(rootB);
        if (rankA < rankB) parent.put(rootA, rootB);
        else if (rankA > rankB) parent.put(rootB, rootA);
        else { parent.put(rootB, rootA); rank.put(rootA, rankA + 1); }
        return true;
    }
}
Komplexes Beispiel 3: Topological Sort mit Cycle-Diagnose

Nützlich für Build-Reihenfolgen, Abhängigkeiten und Workflow-Graphen.

final class TopologicalSort<T> {
    List<T> sort(Map<T, List<T>> graph) {
        var state = new HashMap<T, VisitState>();
        var result = new ArrayList<T>();
        var stack = new ArrayDeque<T>();

        for (T node : graph.keySet()) visit(node, graph, state, result, stack);
        Collections.reverse(result);
        return result;
    }

    private void visit(T node, Map<T, List<T>> graph, Map<T, VisitState> state,
                       List<T> result, Deque<T> stack) {
        VisitState s = state.getOrDefault(node, VisitState.NEW);
        if (s == VisitState.DONE) return;
        if (s == VisitState.ACTIVE) throw new CycleException("Cycle: " + stack + " -> " + node);

        state.put(node, VisitState.ACTIVE);
        stack.push(node);
        for (T next : graph.getOrDefault(node, List.of())) visit(next, graph, state, result, stack);
        stack.pop();
        state.put(node, VisitState.DONE);
        result.add(node);
    }

    enum VisitState { NEW, ACTIVE, DONE }
    static final class CycleException extends RuntimeException { CycleException(String m) { super(m); } }
}
♟️
Finale Erweiterung

Dynamic Programming, Backtracking und Trie

Modul J · Algorithmen & Datenstruktur Mastery · Alt-Referenz: K102

Optimierungs- und Suchprobleme strukturiert lösen.

Dynamic Programming und Backtracking

DP StateTransitionMemoizationBacktracking
Komplexes Beispiel 1: Coin Change Bottom-Up DP

Zeigt Zustandsdefinition, Übergang und unmögliche Zustände.

final class CoinChange {
    int minCoins(int amount, int[] coins) {
        int impossible = amount + 1;
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, impossible);
        dp[0] = 0;

        for (int value = 1; value <= amount; value++) {
            for (int coin : coins) {
                if (coin <= value) dp[value] = Math.min(dp[value], dp[value - coin] + 1);
            }
        }
        return dp[amount] > amount ? -1 : dp[amount];
    }
}
Komplexes Beispiel 2: Backtracking für Kombinationen mit Pruning

Typisch für Suchräume, bei denen nicht jede Möglichkeit vollständig ausprobiert werden muss.

final class CombinationSum {
    List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        var result = new ArrayList<List<Integer>>();
        backtrack(candidates, target, 0, new ArrayList<>(), result);
        return result;
    }

    private void backtrack(int[] nums, int remaining, int start, List<Integer> path, List<List<Integer>> out) {
        if (remaining == 0) { out.add(List.copyOf(path)); return; }
        for (int i = start; i < nums.length; i++) {
            if (nums[i] > remaining) break; // pruning
            path.add(nums[i]);
            backtrack(nums, remaining - nums[i], i, path, out);
            path.removeLast();
        }
    }
}
Komplexes Beispiel 3: Trie Autocomplete

Kombiniert Baumstruktur, sortierte Kinder und limitierte DFS.

final class Trie {
    private final Node root = new Node();

    void insert(String word) {
        Node cur = root;
        for (int cp : word.codePoints().toArray()) {
            cur = cur.children.computeIfAbsent(cp, ignored -> new Node());
        }
        cur.word = true;
    }

    List<String> autocomplete(String prefix, int limit) {
        Node cur = root;
        int[] cps = prefix.codePoints().toArray();
        for (int cp : cps) {
            cur = cur.children.get(cp);
            if (cur == null) return List.of();
        }
        var out = new ArrayList<String>();
        collect(cur, new StringBuilder(prefix), out, limit);
        return out;
    }

    private void collect(Node node, StringBuilder path, List<String> out, int limit) {
        if (out.size() >= limit) return;
        if (node.word) out.add(path.toString());
        for (var entry : node.children.entrySet()) {
            path.appendCodePoint(entry.getKey());
            collect(entry.getValue(), path, out, limit);
            path.setLength(path.length() - Character.charCount(entry.getKey()));
        }
    }

    static final class Node {
        final NavigableMap<Integer, Node> children = new TreeMap<>();
        boolean word;
    }
}
🎤
Finale Erweiterung

Algorithmus Interview Patterns und Musterantworten

Modul J · Algorithmen & Datenstruktur Mastery · Alt-Referenz: K103

Lösungen nicht nur codieren, sondern überzeugend erklären.

AufgabentypErkennungDatenstruktur
Top Khäufigste/größte k ElementeHashMap + PriorityQueue
Range Mergeüberlappende ZeiträumeSort + ArrayList
Shortest Pathgewichtete WegePriorityQueue + Map
ConnectivityKomponenten oder ZyklenUnion-Find
Prefix SearchAutocompleteTrie
Optimizationmin/max mit TeilproblemenDP Tabelle oder Memoization

Interview-Kommunikation

  1. Problem in eigenen Worten wiederholen.
  2. Input, Output und Edge Cases klären.
  3. Brute Force kurz nennen.
  4. Optimierung mit Datenstruktur erklären.
  5. Code sauber schreiben und Invarianten nennen.
  6. Komplexität und Tests abschließen.
Komplexes Beispiel: Interview-Antwort-Template im Code Review Stil

Eine strukturierte Antwort wirkt senioriger als nur schnell Code zu schreiben.

/*
Problem: Finde die k häufigsten Wörter.
Brute Force: Wörter zählen, komplette Liste sortieren -> O(n log n).
Optimierung: HashMap für Frequenzen, Min-Heap Größe k -> O(n log k).
Edge Cases:
- leere Liste -> leere Antwort
- k <= 0 -> leere Antwort oder Validation Error
- gleiche Häufigkeit -> alphabetisch stabil entscheiden
- Groß/Kleinschreibung -> Normalisierung definieren
Proof:
- Heap enthält nach jedem Schritt höchstens k beste Kandidaten der bisher gesehenen Wörter.
- Wenn Heap größer als k wird, entfernen wir den schlechtesten Kandidaten.
*/
Modul K

📚 Quellen, Versionshinweise & Anhang

Quellen, Versionshinweise, Qualitätscheck und ergänzende Hinweise.

1Themen in diesem Modul
0Codeblöcke
0SVG-Grafiken

Quellen und Versionshinweise

Modul K · Quellen, Versionshinweise & Anhang
  • Oracle Java Downloads: JDK 26 ist aktuelle Feature-Version; JDK 25 ist aktuelle LTS-Version.
  • OpenJDK JDK 25 Projektseite: JDK 25 erreichte General Availability am 16. September 2025.
  • OpenJDK JDK 26 Projektseite: JDK 26 erreichte General Availability am 17. März 2026.
  • OpenJDK JEP-Index und JDK-25-JEP-Übersicht: Referenz für Feature-Historie und JEP-Status.

Didaktischer Hinweis: Das Handbuch verwendet moderne Sprachfeatures und markiert Preview-/Feature-Release-Themen nicht als zwingende Produktionsbasis. Für langfristig stabile Projekte ist eine LTS-Version die konservative Grundlage.