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.
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.
Neue Themen werden ergänzt, nicht mitten im Buch eingeschoben.
Interne Links, Lernnotizen und Wiederholungen bleiben gültig.
Deep Dives nach Hauptkapiteln
Grundlagen bleiben lesbar, Profiwissen bekommt eigene Tiefe.
Einsteiger werden nicht überladen, Fortgeschrittene bekommen Substanz.
CommerceFlow als roter Faden
Architektur, 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 getrennt
Aufgaben, 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.
Kontext
Package-Regel
Beispiel
Frameworkfrei / Core Java
com.seb4u.demo[.thema]
package com.seb4u.demo.domain;
Spring Boot / Spring MVC / Spring Data / Spring Security
Jakarta EE / Jakarta Servlet / Jakarta Persistence ohne Spring
com.seb4u.demo.jakarta[.thema]
package com.seb4u.demo.jakarta.web;
Sehr kleine Syntax-Snippets
kein Package
z. 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.
Modul A · Orientierung, Lernarchitektur & Verzeichnis
🧭
So benutzt du dieses Master-Handbuch
Modul A · Orientierung, Lernarchitektur & Verzeichnis · Alt-Referenz: K0
Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.
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.
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.
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.
Java Master-Handbuch
Modul A · Orientierung, Lernarchitektur & Verzeichnis
Final Structure + Final Structure + Fullstack + Cloud + Algorithmus + Print Edition
CoreJava Sprache, Runtime, I/O, Concurrency
EnterpriseSpring, Data, Security, Testing
CloudMicroservices, Messaging, Kubernetes
MasteryAlgorithmen, Interviews, Portfolio
Diese Druckausgabe ist offline-fähig und enthält eingebettete SVG-Diagramme, Codebeispiele, Checklisten und Lernpfade.
🧩
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.
Beibehalten; Kapitel 7-18 dienen als technische Referenz.
Enterprise
Stark ausgebaut
Mit Fullstack- und Cloud-Erweiterung als reale Projektlinie verknüpfen.
Job-Ready
Vorhanden
Mit Algorithmen-Teil und Portfolio-Cases verbinden.
Print
Fehlt als eigener Output
Eigene 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-Element
Umsetzung
Sidebar
Im Print ausgeblendet, weil sie am Papier keinen Nutzen hat.
Copy-Buttons
Im Print ausgeblendet.
SVGs
Bleiben eingebettet und werden seitenbruchgeschützt.
Details/Accordion
Im Print vollständig geöffnet.
Code
Monospace, kleinere Schrift, Zeilenumbruch.
Modul B
☕ Core Java, Sprache & Typsystem
JDK, Syntax, OOP, Records, Collections, Generics, Streams und Fehlerverträge inklusive Deep Dives.
Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K1
Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.
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.
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.
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
publicclassStartPhasen {
static { System.out.println("1) Klasse wird initialisiert"); }
publicstaticvoidmain(String[] args) {
System.out.println("2) main wird ausgeführt");
helper();
}
staticvoidhelper() { 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 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;
publicclassTypenDemo {
publicstaticvoidmain(String[] args) {
int a = 7 / 2; // 3, nicht 3.5double b = 0.1 + 0.2; // nicht exakt 0.3BigDecimal price = newBigDecimal("19.99");
var label = "Preis: " + price; // var ist statisch StringSystem.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 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
sealedinterfacePaymentpermitsCash, Card, BankTransfer {}
recordCash() implementsPayment {}
recordCard(String last4) implementsPayment {}
recordBankTransfer(String iban) implementsPayment {}
classPaymentRouter {
staticStringroute(Payment p) {
returnswitch (p) {
caseCash c -> "Kasse";
caseCard c when c.last4().startsWith("4") -> "Visa-Netzwerk";
caseCard c -> "Kartennetzwerk";
caseBankTransfer 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.
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.
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.
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.
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.
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.
Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K8
Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.
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.
Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K9
Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.
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.
Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K10
Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.
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.
Modul B · Core Java, Sprache & Typsystem · Alt-Referenz: K11
Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.
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.
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.
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.
Konstrukt
Nimm es für
Meide es für
record
Value Objects, DTOs, Events, kleine unveränderliche Antworten
Objekte mit komplexem Lebenszyklus, Identität und mutierendem Zustand
enum
feste Wertebereiche, Status, Strategien mit wenigen Varianten
Mandanten-/DB-konfigurierbare Regeln
sealed interface
geschlossene Ereignis-, Command-, Result- und Fehlerfamilien
Plugin-Systeme mit unbekannten Implementierungen
switch über Typen
exhaustive Fallunterscheidung an Architekturgrenzen
Businesslogik, die durch Polymorphie einfacher wäre
Modelliere zuerst den fachlichen Wert, dann erst die Persistenzform.
Prüfe jede Record-Komponente auf Null, Format und Wertebereich.
Nutze sealed Types für Varianten, die im Release-Zyklus deines Codes kontrolliert werden.
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.
Frage
Typische Wahl
Warum
Schneller Lookup nach Schlüssel?
HashMap
Durchschnittlich konstante Zugriffszeit; Reihenfolge nicht garantiert.
Lookup plus stabile Iterationsreihenfolge?
LinkedHashMap
Ideal für LRU-Caches, Importreihenfolge und deterministische Tests.
Bereichsabfragen nach sortiertem Schlüssel?
TreeMap / NavigableMap
Submaps, floor/ceiling und sortierte Traversierung.
Sehr wenige Enum-Schlüssel?
EnumMap
Kompakt, schnell und typsicher für Enum-Domänen.
Producer/Consumer?
BlockingQueue
Backpressure 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.
Konzept
Bedeutung
Master-Hinweis
Invarianz
List<Dog> ist keine List<Animal>
Schützt davor, falsche Elemente in Collections zu schreiben.
PECS
Producer extends, Consumer super
Fü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 Erasure
Generische Typen sind zur Laufzeit weitgehend gelöscht
Bei Laufzeit-Typprüfung Type Tokens wie Class<T> verwenden.
Benutze Generics, wenn ein Zusammenhang zwischen Parametern ausgedrückt werden muss.
Gib Typvariablen sprechende Namen, sobald die API öffentlich oder komplex wird.
Nutze Wildcards an Konsum-/Produktionsgrenzen, nicht unnötig im Domänenkern.
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.
Situation
Gute Wahl
Warnung
Viele reine Transformationen
stream().map().filter().toList()
Gut lesbar, solange jede Stufe klein und benannt bleibt.
Aggregation mit Zustand
eigener Collector
Zustand kapseln, Combiner korrekt implementieren.
Sehr große Datenquelle
Batching, Iterator, Files.lines mit try-with-resources
Nicht alles mit toList() materialisieren.
I/O pro Element
oft Schleife oder Virtual Threads
Parallel 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.
Fehlerart
Behandlung
Beispiel
Validierungsfehler
sammeln und als 400/Validation Contract zurückgeben
fehlende E-Mail, ungültiges Alter
Domänenkonflikt
fachliche Exception mit Code
Bestellung bereits erfüllt, Version veraltet
Nicht gefunden
Optional intern, 404 an der API-Grenze
UserId existiert nicht
Technische Störung
Retry nur wenn idempotent und retryable
Timeout, temporärer Netzwerkfehler
Programmierfehler
nicht verschlucken
NullPointerException durch Bug, Assertion verletzt
Exceptions nicht als normale Schleifensteuerung missbrauchen.
Fehlercodes stabiler halten als Fehlermeldungen.
Technische Details nicht ungefiltert an API-Clients geben.
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);
}
}
Modul C · Runtime, I/O, Zeit & Concurrency · Alt-Referenz: K12
Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.
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.
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.
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.
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.
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.
`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?
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.
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 CachestaticfinalMap<String, byte[]> CACHE = new java.util.concurrent.ConcurrentHashMap<>();
staticbyte[] load(String key) {
returnCACHE.computeIfAbsent(key, k -> newbyte[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.
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.
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.
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.
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.
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.
Situation
Gute Wahl
Risiko bei schlechter Wahl
Kleine Textdatei
Files.readString mit Charset und Größenlimit
Unbegrenzt lesen, falsches Encoding, OutOfMemory
Große Log-/CSV-Datei
BufferedReader, Zeile für Zeile, Fehler mit Zeilennummer
Alles in Liste laden, Fehlerkontext verlieren
Viele Dateien
Files.walkFileTree mit Fehlerbehandlung
Symlinks/Rechte ignorieren, Abbruch beim ersten Fehler
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);
}
}
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.
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.
Problem
Werkzeug
Professioneller Hinweis
Viele blockierende I/O-Aufgaben
Virtual Threads
Ideal für Request-per-task, aber externe Ressourcen weiterhin limitieren.
Begrenzte Ressource
Semaphore/Bulkhead
Schützt Datenbank, API, Dateisystem oder Legacy-Service.
Erst Zustand vermeiden, dann minimale Synchronisation wählen.
Asynchrone Aggregation
CompletableFuture
Timeouts, 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.
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.
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.
Problem
Gute Lösung
Warum
Sommerzeitwechsel
ZonedDateTime mit echter ZoneId
Offsets ändern sich historisch und zukünftig.
Business Deadline
LocalTime + Kalenderregel + Zone
Deadline ist fachlich, nicht technisch.
Mehrsprachige Ausgabe
DateTimeFormatter und NumberFormat mit Locale
Kein manuelles String-Basteln.
Tests
Clock.fixed oder Clock.offset
Keine 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.
ThroughputWie viele Aufgaben pro Zeitfenster?
ConcurrencyWie viele Aufgaben gleichzeitig?
ParallelismWie viele CPU-Kerne arbeiten wirklich?
ContentionWo konkurrieren Threads um denselben Zustand?
Situation
Passendes Werkzeug
Meisterhinweis
Viele blockierende I/O-Aufrufe
Virtual Threads + Semaphore Bulkhead
Virtual Threads nicht unlimitiert gegen fremde Systeme feuern.
CPU-lastige Arbeit
Fixed Pool nahe CPU-Kernen
Mehr Threads machen CPU-Code nicht schneller.
Shared Counter/Config
Atomic/Immutable Snapshot
Sperren klein halten.
Producer schneller als Consumer
Bounded Queue
Unbounded 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.
Muster
Nutzen
Achtung
thenCompose
Sequenzielle Abhängigkeit flach halten
Nicht mit thenApply verschachteln.
allOf
Parallel warten
Ergebnisse müssen danach typisiert eingesammelt werden.
orTimeout
Latenzbudget erzwingen
Timeout ist kein Abbruch fremder I/O ohne Cancellation.
exceptionally/handle
Fallbacks definieren
Fehler 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.
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.
Ebene
Darf kennen
Darf nicht kennen
Domain
Value Objects, Domain Services
HTTP, SQL, Frameworks, Clock.now direkt
Application
Ports, Transactions, Use Cases
Konkrete Adapterklassen
Adapter
JDBC, Files, HTTP, JSON
Andere Adapter-Interna
Bootstrap
Konkretes Wiring
Fachlogik
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.
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
Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K19
Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.
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;
importstatic org.junit.jupiter.api.Assertions.*;
classMoneyTest {
@TestvoidaddsOnlySameCurrency() {
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-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.
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) thrownewIllegalStateException("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.
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.
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.
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.
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.
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.
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 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);
}
Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K26
Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.
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.
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 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.
Modul D · Professional Backend, Architektur & Code-Labs · Alt-Referenz: K28
Kapitelziel: Verstehen, anwenden, testen und auf reale Projekte übertragen.
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.
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.
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.
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.
Komplexer Beispielcode
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
sealedinterfaceOrderEventpermitsOrderOpened, ItemAdded, OrderSubmitted, PaymentCaptured {
UUIDorderId(); longversion(); InstantoccurredAt();
}
recordOrderOpened(UUID orderId, long version, Instant occurredAt, UUID customerId) implementsOrderEvent {}
recordItemAdded(UUID orderId, long version, Instant occurredAt, String sku, int quantity, long cents) implementsOrderEvent {}
recordOrderSubmitted(UUID orderId, long version, Instant occurredAt) implementsOrderEvent {}
recordPaymentCaptured(UUID orderId, long version, Instant occurredAt, String providerRef) implementsOrderEvent {}
recordSnapshot(UUID orderId, long version, OrderState state) {}
recordOrderState(UUID customerId, List<String> items, boolean submitted, boolean paid, long totalCents) {}
finalclassOrderAggregate {
privateUUID id;
privateUUID customerId;
privatefinalList<String> items = newArrayList<>();
privateboolean submitted;
privateboolean paid;
privatelong totalCents;
privatelong version;
staticOrderAggregatefrom(Snapshot snapshot, List<OrderEvent> laterEvents) {
var aggregate = newOrderAggregate();
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");
returnList.of(newOrderOpened(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");
returnList.of(newItemAdded(id, version + 1, Instant.now(), sku, quantity, cents));
}
List<OrderEvent> submit() {
require(!items.isEmpty(), "empty_order");
require(!submitted, "already_submitted");
returnList.of(newOrderSubmitted(id, version + 1, Instant.now()));
}
List<OrderEvent> capturePayment(String providerRef) {
require(submitted, "not_submitted");
require(!paid, "already_paid");
returnList.of(newPaymentCaptured(id, version + 1, Instant.now(), providerRef));
}
Snapshotsnapshot() {
returnnewSnapshot(id, version, newOrderState(customerId, List.copyOf(items), submitted, paid, totalCents));
}
privatevoidrestore(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();
}
privatevoidapply(OrderEvent event) {
switch (event) {
caseOrderOpened e -> { id = e.orderId(); customerId = e.customerId(); version = e.version(); }
caseItemAdded e -> { items.add(e.sku() + " x" + e.quantity()); totalCents += e.quantity() * e.cents(); version = e.version(); }
caseOrderSubmitted e -> { submitted = true; version = e.version(); }
casePaymentCaptured e -> { paid = true; version = e.version(); }
}
}
privatestaticvoidrequire(boolean ok, String code) { if (!ok) thrownewIllegalStateException(code); }
}
finalclassSalesProjection {
privatefinalMap<UUID, Long> revenueByCustomer = newConcurrentHashMap<>();
privatefinalSet<UUID> paidOrders = ConcurrentHashMap.newKeySet();
voidon(OrderEvent event) {
if (event instanceofPaymentCaptured 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() { returnMap.copyOf(revenueByCustomer); }
}
finalclassVersionedEventStore {
privatefinalMap<UUID, List<OrderEvent>> streams = newConcurrentHashMap<>();
privatefinalAtomicLong globalSequence = newAtomicLong();
synchronizedvoidappend(UUID streamId, long expectedVersion, List<OrderEvent> events) {
var current = newArrayList<>(streams.getOrDefault(streamId, List.of()));
var actual = current.isEmpty() ? 0 : current.getLast().version();
if (actual != expectedVersion) thrownewIllegalStateException("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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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; }
}
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
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 */ }
}
}
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
Stufe
Was du können musst
Typische Fehler
Master-Regel
Framework
Auto-Configuration verstehen, nicht nur Annotationen kopieren
Wichtigste Risiken mit echten Abhängigkeiten testen
Production
Logs, Metrics, Health, Rollback, Secrets
Deployment ohne Runbook
Jeder 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 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.
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.
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.
Thema
JDBC
JPA/Hibernate
Master-Hinweis
Kontrolle
Sehr hoch
Mittel
JDBC für kritische Queries und kleine Domänen
Produktivität
Mittel
Hoch
JPA für CRUD-nahe Aggregate
Performance-Fallen
Manuelles Mapping
N+1, Lazy Loading
Immer SQL beobachten
Transaktionen
Explizit
Deklarativ
Use-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);
Testing Masterclass: Unit, Integration, Contract, Architektur
Modul F · Enterprise Java: Spring, Persistence, Testing, Security & Production · Alt-Referenz: K68
Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.
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");
}
}
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 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.
Risiko
Schutz
Code-Ort
Fehler
Injection
Prepared Statements, Validation
Repository / API DTO
String-Konkatenation in SQL
Broken Access Control
Fachliche Policies
Application Service
Nur URL-Rollen prüfen
Credential Leak
Hashing, Secret Manager
Identity Adapter
Passwörter loggen
JWT Missbrauch
Issuer, Audience, Expiry, Key Rotation
Security Config
Token ohne Claims prüfen
Code 1: Spring Security Konfiguration mit klaren Grenzen
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 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();
}
}
Modul F · Enterprise Java: Spring, Persistence, Testing, Security & Production · Alt-Referenz: K71
Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.
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.
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;
}
}
Modul F · Enterprise Java: Spring, Persistence, Testing, Security & Production · Alt-Referenz: K72
Professional-Erweiterung: praxisnah, architekturbewusst und joborientiert.
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?
Gate
Frage
Akzeptanzkriterium
Build
Ist das Artefakt reproduzierbar?
Clean Build auf CI, Lockfile/BOM, keine lokalen Pfade
Test
Sind Kernrisiken getestet?
Domain, Persistence, API, Security, Migration
Deploy
Kann die Version sicher starten?
Health UP, Migration erfolgreich, Config validiert
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.
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.
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.
// 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.
Frage
Worauf Interviewer achten
Starke Antwort
Wann nutzt du Records?
Immutability, Value Semantics
Für transparente Datenwerte, DTOs und Value Objects; nicht für komplexe Aggregate mit Identität und Lebenszyklus.
Wie vermeidest du N+1?
ORM-Verständnis
Fetch Join, Entity Graph, DTO Query, Batch Size, SQL Logging und Testdaten mit mehreren Relationen.
Was ist eine Transaktionsgrenze?
Use-Case-Denken
Eine fachlich atomare Änderung. Nicht Controller, nicht Repository-Methode, sondern Application Service.
Virtual Threads vs Platform Threads?
Concurrency-Modell
Virtual 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.
🏛️
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.
Entscheidung
Gute Begründung
Warnsignal
Modularer Monolith
Ein Deployment, klare Codegrenzen
Teams blockieren sich ständig
Microservice
Unabhängige Skalierung und Ownership
Verteilte Transaktionen nötig
Event-Driven
Lose Kopplung, Audit-Trail
Fehlerhafte 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.
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.
Artefakt
Was es beweist
Mindestqualität
README
Kommunikation
Setup, Features, Architekturdiagramm, Commands
Tests
Qualitätsdenken
Unit + Integration + API Tests
Docker Compose
Reproduzierbarkeit
App + Postgres + optional Redis/Kafka
ADR
Architekturdenken
Mindestens 3 Entscheidungen mit Konsequenzen
Runbook
Production Mindset
Health, 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.“
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(); }
}
Problem
Pattern
Wichtig
Langsamer Downstream
Timeout + Circuit Breaker
Nie unendlich warten.
Überlastung
Rate Limiting + Bulkhead
Fehler isolieren statt global eskalieren.
Netzwerkfehler
Retry mit Backoff
Nur idempotente Operationen automatisch wiederholen.
Dauerhafte Fehler
Dead-letter + Alert
Nicht 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.
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.
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.
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.
Aufgabentyp
Erkennung
Datenstruktur
Top K
häufigste/größte k Elemente
HashMap + PriorityQueue
Range Merge
überlappende Zeiträume
Sort + ArrayList
Shortest Path
gewichtete Wege
PriorityQueue + Map
Connectivity
Komponenten oder Zyklen
Union-Find
Prefix Search
Autocomplete
Trie
Optimization
min/max mit Teilproblemen
DP Tabelle oder Memoization
Interview-Kommunikation
Problem in eigenen Worten wiederholen.
Input, Output und Edge Cases klären.
Brute Force kurz nennen.
Optimierung mit Datenstruktur erklären.
Code sauber schreiben und Invarianten nennen.
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.
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.