M01 · Orientierung
Lernziel: Du verstehst, warum Java 11, 17 und 21 zusammen gelernt werden.
Praxis: Erstelle eine persönliche Zielmatrix: Backend, Framework, Portfolio.
Abgabe: Eine kurze Lernvereinbarung im README.
Eine zentrale Offline-HTML mit sichtbarem Lernpfad, eigenem Theorie-Teil, Akademie-Modulen, Code-Labs, Framework-Tracks, Referenzteil und eingebettetem TaskFlow-Portfolio-Projekt.
Dieser Bereich ist der eigentliche Fahrplan. Die Akademie-Module erklären die Themen tiefer; der Lernpfad sagt dir, in welcher Reihenfolge du vorgehen solltest und was am Ende jeder Phase stehen muss.
Erst Pure Java und Domain-Verständnis, dann Java 21 Runtime, danach Spring/Jakarta als Adapter. Dadurch lernst du Frameworks nicht als Magie, sondern als Umsetzung einer sauberen Architektur.
0/12 Module erledigt
| Zeitraum | Thema | Lernziel | Ergebnis |
|---|---|---|---|
| Woche 1 | Orientierung & Setup | JDKs, IDE, Git, Maven, Projekt importieren | Projekt lokal öffnen und CLI starten |
| Woche 2 | Java-11-Basis | var, String/Files API, Optional, Streams | kleine Datei-/CSV-Übungen |
| Woche 3 | HTTP & I/O | Java 11 HttpClient, Fehlerfälle, Timeouts | RemoteTaskClient erweitern |
| Woche 4 | Testing-Grundlage | JUnit, Testdaten, einfache CI | erste Tests grün bekommen |
| Woche 5 | Java-17-Domain | Records, Invarianten, Value Objects | Task-Domain erklären können |
| Woche 6 | Sealed Design | sealed interfaces/classes, Events, Policies | neues Domain-Event ergänzen |
| Woche 7 | Architektur | Ports/Adapter, Layering, Package-Regel | Architekturdiagramm verstehen |
| Woche 8 | Refactoring | Duplikate entfernen, Fehlerbehandlung, README | sauberer Zwischenstand |
| Woche 9 | Java 21 | Pattern Matching for switch, Sequenced Collections | Command Handler erweitern |
| Woche 10 | Virtual Threads | Batch-Verarbeitung, Limits, Fehler sammeln | Runtime-Lab abschließen |
| Woche 11 | Spring Track | Controller, DTO, Validation, ExceptionHandler | REST-Endpunkte dokumentieren |
| Woche 12 | Jakarta Track | JAX-RS, CDI, WAR, Validation | Jakarta API vergleichen |
| Woche 13 | Migration | 11 → 17 → 21, Framework-Entscheidungen | Migrations-Checkliste |
| Woche 14 | Qualität | Tests, CI, Code Review, Package Audit | CI und Testplan |
| Woche 15 | Portfolio | README, Diagramme, GitHub-Struktur | Projekt präsentierbar machen |
| Woche 16 | Abschluss | Demo, Interviewfragen, Rückblick | Portfolio-Abgabe |
Dieser Abschnitt ist der fehlende theoretische Kern der Masterclass. Er erklärt die Konzepte hinter den Labs, damit du nicht nur Code kopierst, sondern Entscheidungen begründen kannst: warum Java 11, 17 und 21 unterschiedlich eingesetzt werden, wie Domain-Code stabil bleibt und wann Pure Java, Spring oder Jakarta sinnvoll sind.
Java-Code wird zu Bytecode kompiliert und von der JVM ausgeführt. Wichtig sind Classpath/Module-Path, Heap, Stack, Garbage Collection, JIT-Optimierung und der Unterschied zwischen Compile-Time- und Runtime-Fehlern. Dieses Modell hilft dir, Performance, Speicher und Deployment-Probleme einzuordnen.
Java 11 ist die stabile Grundlage für produktive Enterprise-Systeme: HTTP Client, Files API, Optional, Streams, Collection Factories und saubere Service-Klassen. Theoretisch lernst du hier API-Design, Nebenwirkungsarmut, Fehlergrenzen und einfache Datenflüsse.
Records reduzieren Boilerplate für unveränderliche Daten. Sealed Interfaces begrenzen erlaubte Untertypen und machen Fachmodelle ausdrücklicher. Pattern Matching verbessert Lesbarkeit, wenn Typprüfungen fachlich wirklich notwendig sind.
Virtual Threads vereinfachen blockierende I/O-lastige Aufgaben, ohne sofort reaktive Programmierung zu erzwingen. Pattern Matching for switch macht Entscheidungslogik lesbarer. Wichtig bleibt: Virtual Threads lösen keine Datenrennen, sondern primär Skalierungsprobleme bei wartenden Tasks.
Gute Java-Modelle kapseln Regeln dort, wo sie entstehen. Entities besitzen Identität, Value Objects beschreiben Werte, Services koordinieren Abläufe. Immutability reduziert Seiteneffekte und macht Tests, Parallelisierung und Fehleranalyse einfacher.
Exceptions sollten Grenzen markieren: ungültige Eingaben, fehlende Ressourcen, technische Fehler. In der Domain sind klare Fehlermeldungen wichtiger als Framework-spezifische Exceptions. Controller oder Resources übersetzen diese Fehler später in HTTP-Antworten.
Collections modellieren Mengen, Reihenfolgen und Schlüssel-Wert-Beziehungen. Generics geben Typsicherheit. Streams sind für deklarative Transformationen stark, aber nicht jede Schleife muss ein Stream werden. Lesbarkeit bleibt die wichtigste Regel.
Die Masterclass folgt einer einfachen Trennung: Domain kennt keine Frameworks, Application/Service koordiniert Use Cases, Adapter verbinden CLI, REST, Dateiablage oder HTTP. Dadurch bleibt das Kernmodell wiederverwendbar.
Spring Boot bringt einen Application Context, Dependency Injection, Auto-Configuration und Spring MVC. Das ist ideal, wenn du schnell produktive REST-APIs, Konfiguration, Tests und Integration brauchst. Die Domain sollte trotzdem nicht von Spring abhängen.
Jakarta EE standardisiert APIs wie JAX-RS, CDI und Validation. Es passt gut, wenn Standardisierung, Application Server oder Herstellerneutralität wichtig sind. Auch hier bleibt die Domain frameworkfrei; Jakarta sitzt als Adapter darüber.
Viele schnelle Unit-Tests prüfen Fachlogik. Weniger Integrationstests prüfen Zusammenspiel mit Frameworks, HTTP oder Dateiablage. End-to-End-Tests sind wertvoll, aber teuer. Gute Tests erklären Verhalten, nicht Implementierungsdetails.
Migration bedeutet nicht nur JDK wechseln. Du prüfst Build-Plugins, Libraries, Reflection, entfernte APIs, CI-Versionen und Laufzeitverhalten. Der sichere Weg ist: erst kompilieren, dann Tests, dann Runtime, dann Performance und Observability.
| Thema | Du solltest erklären können | Im Projekt sichtbar in |
|---|---|---|
| Java 11 | Dateien lesen/schreiben, HTTP-Aufrufe kapseln, Daten transformieren | taskflow-foundation |
| Java 17 | Records, sealed Events und fachliche Invarianten | taskflow-domain |
| Java 21 | Virtual Threads, Command-Verarbeitung und moderne Switch-Logik | taskflow-runtime |
| Spring | Controller, Service, Validation und Exception Handler als Adapter | taskflow-spring-api |
| Jakarta | JAX-RS Resource, CDI Service, Validation und ExceptionMapper | taskflow-jakarta-api |
Die Lernlandkarte zeigt den Zusammenhang zwischen Lernpfad, Modulen und Projekt. Du gehst nicht zufällig durch Themen, sondern baust eine durchgehende Codebasis auf.
Alle Packages folgen deiner Regel. Frameworkfreier Code bleibt unter com.seb4u.demo, Spring-Code unter com.seb4u.demo.spring, Jakarta-Code unter com.seb4u.demo.jakarta.
com.seb4u.demo.<projekt>.<bereich> # Pure Java / kein Framework
com.seb4u.demo.spring.<projekt>.<bereich> # Spring
com.seb4u.demo.jakarta.<projekt>.<bereich> # Jakarta EEDie Masterclass trennt Sprachebene und Framework-Ebene. Java 11 ist Grundlage für moderne APIs, Java 17 für Domain-Modellierung und Java 21 für moderne Nebenläufigkeit.
| Ebene | Projektteil | Inhalte | Build-Hinweis |
|---|---|---|---|
| Java 11 | Foundation | HttpClient, Files, Optional, Streams | maven.compiler.release=11 |
| Java 17 | Domain, Spring, Jakarta | Records, sealed Types, Validation, REST | release=17 |
| Java 21 | Runtime | Virtual Threads, Pattern Matching for switch | release=21 |
TaskFlow ist bewusst als Portfolioprojekt aufgebaut: die Domain bleibt frameworkfrei, die Adapter zeigen CLI, Spring und Jakarta als verschiedene Außenkanten.
Records, Value Objects, sealed Events und Policies.
Java 21 Commands und Virtual-Thread-Verarbeitung.
CLI, Spring REST und Jakarta REST verwenden dieselbe Fachlogik.
Die Akademie ist die Detailansicht zum Lernpfad. Jedes Modul hat ein Ziel, eine Praxisaufgabe und eine klare Abgabe.
Lernziel: Du verstehst, warum Java 11, 17 und 21 zusammen gelernt werden.
Praxis: Erstelle eine persönliche Zielmatrix: Backend, Framework, Portfolio.
Abgabe: Eine kurze Lernvereinbarung im README.
Lernziel: Du kannst JDKs, Maven, IDE und Projektstruktur einrichten.
Praxis: Importiere das Projekt und öffne die Module separat.
Abgabe: CLI startet lokal.
Lernziel: Du nutzt stabile moderne APIs wie Files, HttpClient, Optional und Streams.
Praxis: Erweitere den CSV-Repository-Code um Suchfunktionen.
Abgabe: Ein getestetes Repository.
Lernziel: Du schreibst gezielte Unit Tests ohne Framework-Magie.
Praxis: Teste Validierung, CSV-Escaping und Fehlerfälle.
Abgabe: JUnit-Tests für Domain/Foundation.
Lernziel: Du modellierst Fachlogik mit Records und klaren Invarianten.
Praxis: Füge Prioritätsregeln und Value Objects hinzu.
Abgabe: Eine erklärbare Domain.
Lernziel: Du begrenzt erlaubte Event-Typen bewusst.
Praxis: Ergänze TaskArchived und passe Policies an.
Abgabe: Ein geschlossenes Event-Modell.
Lernziel: Du trennst Domain, Runtime, CLI und Framework-Adapter.
Praxis: Zeichne Abhängigkeiten und verbiete Framework-Code in der Domain.
Abgabe: Architektur-Review.
Lernziel: Du nutzt Pattern Matching for switch und Virtual Threads verantwortungsvoll.
Praxis: Erweitere Batch-Verarbeitung mit Fehleraggregation.
Abgabe: Runtime-Lab.
Lernziel: Du baust eine REST-API mit Controller, DTOs, Validation und Handler.
Praxis: Ergänze Update-Endpunkt und Fehlerantworten.
Abgabe: Spring Track lauffähig.
Lernziel: Du verstehst JAX-RS, CDI, Validation und WAR-Deployment.
Praxis: Ergänze Resource-Methoden und Mapper.
Abgabe: Jakarta Track nachvollziehbar.
Lernziel: Du entscheidest zwischen Pure Java, Spring und Jakarta passend zum Kontext.
Praxis: Baue eine Entscheidungstabelle.
Abgabe: Migrationsnotizen.
Lernziel: Du präsentierst Code, Architektur, Tests und Lernfortschritt sauber.
Praxis: README finalisieren und Demo-Skript schreiben.
Abgabe: Portfolio-fertige Abgabe.
Der Pure-Java-Track zeigt die Sprache ohne Framework-Überbau. Hier entstehen Domain, Foundation, Runtime und CLI.
package com.seb4u.demo.taskflow.domain;
public record Task(TaskId id, String title, TaskStatus status, int priority) {
public Task {
if (id == null) {
throw new IllegalArgumentException("id must not be null");
}
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("title must not be blank");
}
if (status == null) {
throw new IllegalArgumentException("status must not be null");
}
if (priority < 1 || priority > 5) {
throw new IllegalArgumentException("priority must be between 1 and 5");
}
title = title.trim();
}
public Task start() {
return new Task(id, title, TaskStatus.IN_PROGRESS, priority);
}
public Task complete() {
return new Task(id, title, TaskStatus.DONE, priority);
}
public Task withPriority(int newPriority) {
return new Task(id, title, status, newPriority);
}
public boolean isOpen() {
return status != TaskStatus.DONE;
}
}
package com.seb4u.demo.taskflow.domain;
import java.time.Instant;
public sealed interface TaskEvent permits TaskCreated, TaskCompleted, PriorityChanged {
TaskId taskId();
Instant occurredAt();
}
package com.seb4u.demo.taskflow.foundation;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
public final class FileTaskRepository {
public List<StoredTask> load(Path file) throws IOException {
if (Files.notExists(file)) {
return List.of();
}
try (var lines = Files.lines(file, StandardCharsets.UTF_8)) {
return lines
.filter(line -> !line.isBlank())
.map(StoredTask::fromCsvLine)
.collect(Collectors.toUnmodifiableList());
}
}
public void save(Path file, List<StoredTask> tasks) throws IOException {
Path parent = file.getParent();
if (parent != null) {
Files.createDirectories(parent);
}
List<String> lines = tasks.stream()
.map(StoredTask::toCsvLine)
.collect(Collectors.toList());
Files.write(file, lines, StandardCharsets.UTF_8);
}
}
package com.seb4u.demo.taskflow.runtime;
import com.seb4u.demo.taskflow.domain.Task;
import com.seb4u.demo.taskflow.domain.TaskStatus;
public final class TaskCommandHandler {
public Task apply(Task current, TaskCommand command) {
return switch (command) {
case AddTask add -> new Task(add.id(), add.title(), TaskStatus.TODO, add.priority());
case CompleteTask complete when current.id().equals(complete.id()) -> current.complete();
case ReprioritizeTask reprioritize when current.id().equals(reprioritize.id()) -> current.withPriority(reprioritize.priority());
default -> current;
};
}
}
package com.seb4u.demo.taskflow.runtime;
import com.seb4u.demo.taskflow.domain.Task;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Supplier;
public final class VirtualTaskProcessor {
public List<Task> process(List<Supplier<Task>> jobs) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<Task>> futures = jobs.stream()
.map(job -> executor.submit(job::get))
.toList();
List<Task> result = new ArrayList<>();
for (Future<Task> future : futures) {
result.add(await(future));
}
return List.copyOf(result);
}
}
private Task await(Future<Task> future) {
try {
return future.get();
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Task processing interrupted", exception);
} catch (ExecutionException exception) {
throw new IllegalStateException("Task processing failed", exception.getCause());
}
}
}
Der Spring Track setzt denselben Use Case als REST-API um. Package: com.seb4u.demo.spring.taskflow....
package com.seb4u.demo.spring.taskflow.api;
import com.seb4u.demo.spring.taskflow.service.SpringTaskService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final SpringTaskService service;
public TaskController(SpringTaskService service) {
this.service = service;
}
@GetMapping
public List<TaskResponse> all() {
return service.findAll().stream().map(TaskResponse::from).toList();
}
@GetMapping("/{id}")
public TaskResponse byId(@PathVariable String id) {
return service.findById(id)
.map(TaskResponse::from)
.orElseThrow(() -> new IllegalArgumentException("Task not found: " + id));
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public TaskResponse create(@Valid @RequestBody TaskRequest request) {
return TaskResponse.from(service.create(request.title(), request.priority()));
}
@PostMapping("/{id}/complete")
public TaskResponse complete(@PathVariable String id) {
return TaskResponse.from(service.complete(id));
}
}
Der Jakarta Track zeigt denselben Use Case mit JAX-RS/CDI und WAR-Deployment. Package: com.seb4u.demo.jakarta.taskflow....
package com.seb4u.demo.jakarta.taskflow.api;
import com.seb4u.demo.jakarta.taskflow.service.JakartaTaskService;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.net.URI;
import java.util.List;
@Path("/tasks")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class TaskResource {
private JakartaTaskService service;
public TaskResource() {
}
@Inject
public TaskResource(JakartaTaskService service) {
this.service = service;
}
@GET
public List<TaskResponse> all() {
return service.findAll().stream().map(TaskResponse::from).toList();
}
@GET
@Path("/{id}")
public TaskResponse byId(@PathParam("id") String id) {
return service.findById(id)
.map(TaskResponse::from)
.orElseThrow(() -> new IllegalArgumentException("Task not found: " + id));
}
@POST
public Response create(@Valid TaskRequest request) {
var created = service.create(request.title(), request.priority());
return Response.created(URI.create("/api/tasks/" + created.id().value()))
.entity(TaskResponse.from(created))
.build();
}
@POST
@Path("/{id}/complete")
public TaskResponse complete(@PathParam("id") String id) {
return TaskResponse.from(service.complete(id));
}
}
Alle drei Tracks verwenden denselben TaskFlow-Use-Case. Der Unterschied liegt im Adapter und im Deployment-Modell.
| Thema | Pure Java | Spring | Jakarta |
|---|---|---|---|
| Package | com.seb4u.demo.taskflow | com.seb4u.demo.spring.taskflow | com.seb4u.demo.jakarta.taskflow |
| API | CLI / eigene Adapter | Spring MVC Controller | JAX-RS Resource |
| DI | Konstruktoren | Spring Container | CDI Container |
| Validation | eigene Invarianten | Jakarta Validation via Spring | Jakarta Validation |
| Deployment | JAR / CLI | Boot JAR | WAR / Jakarta Runtime |
Die Labs sind praktische Etappen im Portfolio-Projekt. Sie passen direkt zum Lernpfad.
Fokus: Domain + Foundation
Aufgabe: Erweitere StoredTask um dueDate und teste CSV-Escaping.
Fokus: Records + sealed
Aufgabe: Füge TaskArchived hinzu und dokumentiere Policy-Regeln.
Fokus: switch + Virtual Threads
Aufgabe: Batch-Verarbeitung soll Fehler sammeln statt abbrechen.
Fokus: Controller + Service + DTOs
Aufgabe: Implementiere PUT /api/tasks/{id}/priority.
Fokus: JAX-RS + CDI + WAR
Aufgabe: Ergänze Validation-ExceptionMapper.
Fokus: README + CI + Doku
Aufgabe: Baue eine Demo-Story für GitHub und Interview.
Tests sollen zuerst die Domain absichern, dann Adapter und API-Verhalten. Die CI-Datei ist im Projekt enthalten.
Am Ende steht kein loses Skript, sondern ein GitHub-taugliches Multi-Modul-Projekt mit README, CI, Docs und nachvollziehbaren Lernspuren.
Die wichtigsten Dateien aus dem ZIP sind direkt eingebettet. Die Codeblöcke sind bewusst sauber kopierbar und enthalten keinen fehlerhaften HTML-Span-Code.
name: taskflow-ci
on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
cache: maven
- name: Build and test all modules
run: mvn -B verify
# TaskFlow Java/Spring/Jakarta Masterclass
Ein Portfolio-Projekt für **Java 11**, **Java 17**, **Java 21**, **Spring Boot** und **Jakarta EE**.
## Package-Regel
```text
com.seb4u.demo.<projekt>.<bereich> # Pure Java / kein Framework
com.seb4u.demo.spring.<projekt>.<bereich> # Spring
com.seb4u.demo.jakarta.<projekt>.<bereich> # Jakarta EE
```
Dieses Projekt nutzt konkret:
```text
com.seb4u.demo.taskflow... # Domain, Java 11 Foundation, Java 21 Runtime, CLI
com.seb4u.demo.spring.taskflow... # Spring Boot REST API
com.seb4u.demo.jakarta.taskflow... # Jakarta REST/CDI API
```
## Modulstruktur
```text
taskflow-java-masterclass/
├── taskflow-foundation/ # Java 11: HttpClient, Files, Collections
├── taskflow-domain/ # Java 17: records, sealed interfaces, Pattern Matching
├── taskflow-runtime/ # Java 21: Virtual Threads, Sequenced Collections, switch Pattern Matching
├── taskflow-cli/ # CLI-Demo
├── taskflow-spring-api/ # Spring Boot REST API
├── taskflow-jakarta-api/ # Jakarta EE REST/CDI API
├── docs/ # Masterclass HTML/Markdown
└── .github/workflows/ci.yml # CI mit JDK 21
```
## Start
```bash
mvn -B verify
mvn -pl taskflow-cli -am exec:java
```
Spring API lokal starten:
```bash
mvn -pl taskflow-spring-api -am spring-boot:run
curl -X POST http://localhost:8080/api/tasks \
-H 'Content-Type: application/json' \
-d '{"title":"Spring Lab abschließen","priority":2}'
```
Jakarta API bauen:
```bash
mvn -pl taskflow-jakarta-api -am package
```
Das erzeugt `taskflow-jakarta-api/target/taskflow-jakarta-api.war`, das in einer Jakarta-EE-11-kompatiblen Runtime deployed werden kann.
## Lernidee
Alle drei Varianten nutzen denselben Use Case: **TaskFlow Task API**.
- Pure Java zeigt Sprache, Datenmodell, Dateien, HTTP Client und Java-21-Concurrency.
- Spring zeigt Controller, Service, Validation, Boot-Konventionen und runnable API.
- Jakarta zeigt JAX-RS, CDI, Jakarta Validation und WAR-Deployment.
## Dokumentation
Öffne:
```text
docs/java_masterclass_11_17_21.html
```
Die HTML-Datei ist offline nutzbar und enthält Architekturdiagramme, Code-Labs, Vergleichstabellen und Projektdateien.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.seb4u.demo</groupId>
<artifactId>taskflow-java-masterclass</artifactId>
<version>1.1.0</version>
<packaging>pom</packaging>
<name>TaskFlow Java/Spring/Jakarta Masterclass</name>
<description>Portfolio-Projekt für Java 11, Java 17, Java 21, Spring und Jakarta EE</description>
<modules>
<module>taskflow-foundation</module>
<module>taskflow-domain</module>
<module>taskflow-runtime</module>
<module>taskflow-cli</module>
<module>taskflow-spring-api</module>
<module>taskflow-jakarta-api</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>21</maven.compiler.release>
<junit.jupiter.version>6.1.1</junit.jupiter.version>
<spring-boot.version>4.1.0</spring-boot.version>
<jakarta.ee.version>11.0.0</jakarta.ee.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>${maven.compiler.release}</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<useModulePath>false</useModulePath>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.3.0</version>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.seb4u.demo</groupId>
<artifactId>taskflow-java-masterclass</artifactId>
<version>1.1.0</version>
</parent>
<artifactId>taskflow-cli</artifactId>
<name>TaskFlow CLI</name>
<properties><maven.compiler.release>21</maven.compiler.release></properties>
<dependencies>
<dependency>
<groupId>com.seb4u.demo</groupId>
<artifactId>taskflow-domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.seb4u.demo</groupId>
<artifactId>taskflow-foundation</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.seb4u.demo</groupId>
<artifactId>taskflow-runtime</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<configuration>
<mainClass>com.seb4u.demo.taskflow.cli.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
package com.seb4u.demo.taskflow.cli;
import com.seb4u.demo.taskflow.domain.Task;
import com.seb4u.demo.taskflow.domain.TaskId;
import com.seb4u.demo.taskflow.domain.TaskStatus;
import com.seb4u.demo.taskflow.foundation.FileTaskRepository;
import com.seb4u.demo.taskflow.foundation.StoredTask;
import com.seb4u.demo.taskflow.foundation.TaskTableFormatter;
import com.seb4u.demo.taskflow.runtime.VirtualTaskProcessor;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Supplier;
public final class Main {
private Main() {
}
public static void main(String[] args) throws Exception {
List<Task> tasks = List.of(
new Task(new TaskId("T-100"), "Java 11 HTTP Client wiederholen", TaskStatus.TODO, 2),
new Task(new TaskId("T-200"), "Record-basiertes Domain-Modell bauen", TaskStatus.IN_PROGRESS, 1),
new Task(new TaskId("T-300"), "Virtual Threads Lab abschließen", TaskStatus.TODO, 3)
);
var repository = new FileTaskRepository();
var file = Path.of("target", "taskflow.csv");
repository.save(file, tasks.stream().map(Main::toStoredTask).toList());
var loaded = repository.load(file);
System.out.println(new TaskTableFormatter().asTable(loaded));
List<Supplier<Task>> jobs = List.of(
() -> new Task(TaskId.random(), "Import Batch A", TaskStatus.TODO, 2),
() -> new Task(TaskId.random(), "Import Batch B", TaskStatus.TODO, 2),
() -> new Task(TaskId.random(), "Import Batch C", TaskStatus.TODO, 2)
);
var processed = new VirtualTaskProcessor().process(jobs);
System.out.println();
System.out.println("Virtual-Thread-Ergebnis: " + processed.size() + " Tasks erzeugt");
}
private static StoredTask toStoredTask(Task task) {
return new StoredTask(task.id().value(), task.title(), task.status().name(), task.priority());
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.seb4u.demo</groupId>
<artifactId>taskflow-java-masterclass</artifactId>
<version>1.1.0</version>
</parent>
<artifactId>taskflow-domain</artifactId>
<name>TaskFlow Domain - Java 17</name>
<properties><maven.compiler.release>17</maven.compiler.release></properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
package com.seb4u.demo.taskflow.domain;
import java.time.Instant;
public record PriorityChanged(TaskId taskId, int oldPriority, int newPriority, Instant occurredAt) implements TaskEvent {
public PriorityChanged {
if (oldPriority < 1 || oldPriority > 5 || newPriority < 1 || newPriority > 5) {
throw new IllegalArgumentException("priorities must be between 1 and 5");
}
}
}
package com.seb4u.demo.taskflow.domain;
public record Task(TaskId id, String title, TaskStatus status, int priority) {
public Task {
if (id == null) {
throw new IllegalArgumentException("id must not be null");
}
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("title must not be blank");
}
if (status == null) {
throw new IllegalArgumentException("status must not be null");
}
if (priority < 1 || priority > 5) {
throw new IllegalArgumentException("priority must be between 1 and 5");
}
title = title.trim();
}
public Task start() {
return new Task(id, title, TaskStatus.IN_PROGRESS, priority);
}
public Task complete() {
return new Task(id, title, TaskStatus.DONE, priority);
}
public Task withPriority(int newPriority) {
return new Task(id, title, status, newPriority);
}
public boolean isOpen() {
return status != TaskStatus.DONE;
}
}
package com.seb4u.demo.taskflow.domain;
import java.time.Instant;
public record TaskCompleted(TaskId taskId, Instant occurredAt) implements TaskEvent {
}
package com.seb4u.demo.taskflow.domain;
import java.time.Instant;
public record TaskCreated(TaskId taskId, String title, Instant occurredAt) implements TaskEvent {
public TaskCreated {
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("title must not be blank");
}
}
}
package com.seb4u.demo.taskflow.domain;
import java.time.Instant;
public sealed interface TaskEvent permits TaskCreated, TaskCompleted, PriorityChanged {
TaskId taskId();
Instant occurredAt();
}
package com.seb4u.demo.taskflow.domain;
import java.util.UUID;
public record TaskId(String value) {
public TaskId {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("TaskId must not be blank");
}
value = value.trim();
}
public static TaskId random() {
return new TaskId(UUID.randomUUID().toString().substring(0, 8));
}
}
package com.seb4u.demo.taskflow.domain;
public final class TaskPolicy {
public String describe(TaskEvent event) {
if (event instanceof TaskCreated created) {
return "Task created: " + created.title();
}
if (event instanceof TaskCompleted completed) {
return "Task completed: " + completed.taskId().value();
}
if (event instanceof PriorityChanged changed) {
return "Priority changed from " + changed.oldPriority() + " to " + changed.newPriority();
}
throw new IllegalArgumentException("Unsupported event: " + event);
}
public boolean canStart(Task task) {
return task.status() == TaskStatus.TODO && task.priority() <= 3;
}
}
package com.seb4u.demo.taskflow.domain;
public enum TaskStatus {
TODO,
IN_PROGRESS,
DONE
}
package com.seb4u.demo.taskflow.domain;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TaskPolicyTest {
@Test
void describesCreatedEvent() {
var policy = new TaskPolicy();
var event = new TaskCreated(new TaskId("T-1"), "Records lernen", Instant.EPOCH);
assertEquals("Task created: Records lernen", policy.describe(event));
}
@Test
void startsOnlyImportantTodoTasks() {
var task = new Task(new TaskId("T-2"), "Sealed Types", TaskStatus.TODO, 3);
assertTrue(new TaskPolicy().canStart(task));
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.seb4u.demo</groupId>
<artifactId>taskflow-java-masterclass</artifactId>
<version>1.1.0</version>
</parent>
<artifactId>taskflow-foundation</artifactId>
<name>TaskFlow Foundation - Java 11</name>
<properties><maven.compiler.release>11</maven.compiler.release></properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
package com.seb4u.demo.taskflow.foundation;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
public final class FileTaskRepository {
public List<StoredTask> load(Path file) throws IOException {
if (Files.notExists(file)) {
return List.of();
}
try (var lines = Files.lines(file, StandardCharsets.UTF_8)) {
return lines
.filter(line -> !line.isBlank())
.map(StoredTask::fromCsvLine)
.collect(Collectors.toUnmodifiableList());
}
}
public void save(Path file, List<StoredTask> tasks) throws IOException {
Path parent = file.getParent();
if (parent != null) {
Files.createDirectories(parent);
}
List<String> lines = tasks.stream()
.map(StoredTask::toCsvLine)
.collect(Collectors.toList());
Files.write(file, lines, StandardCharsets.UTF_8);
}
}
package com.seb4u.demo.taskflow.foundation;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public final class RemoteTaskClient {
private final HttpClient httpClient;
public RemoteTaskClient() {
this(HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.followRedirects(HttpClient.Redirect.NORMAL)
.build());
}
public RemoteTaskClient(HttpClient httpClient) {
this.httpClient = httpClient;
}
public String fetchJson(URI uri) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder(uri)
.timeout(Duration.ofSeconds(5))
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new IOException("HTTP " + response.statusCode() + " for " + uri);
}
return response.body();
}
}
package com.seb4u.demo.taskflow.foundation;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public final class StoredTask {
private final String id;
private final String title;
private final String status;
private final int priority;
public StoredTask(String id, String title, String status, int priority) {
this.id = requireText(id, "id");
this.title = requireText(title, "title");
this.status = requireText(status, "status");
if (priority < 1 || priority > 5) {
throw new IllegalArgumentException("priority must be between 1 and 5");
}
this.priority = priority;
}
public String id() { return id; }
public String title() { return title; }
public String status() { return status; }
public int priority() { return priority; }
public String toCsvLine() {
return String.join(";", escape(id), escape(title), escape(status), Integer.toString(priority));
}
public static StoredTask fromCsvLine(String line) {
List<String> parts = splitEscaped(Objects.requireNonNull(line, "line"));
if (parts.size() != 4) {
throw new IllegalArgumentException("Invalid task CSV line: " + line);
}
return new StoredTask(parts.get(0), parts.get(1), parts.get(2), Integer.parseInt(parts.get(3)));
}
private static String requireText(String value, String field) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(field + " must not be blank");
}
return value.trim();
}
private static String escape(String value) {
StringBuilder escaped = new StringBuilder(value.length());
for (int index = 0; index < value.length(); index++) {
char character = value.charAt(index);
if (character == '\\' || character == ';') {
escaped.append('\\');
}
escaped.append(character);
}
return escaped.toString();
}
private static List<String> splitEscaped(String line) {
List<String> parts = new ArrayList<>();
StringBuilder current = new StringBuilder();
boolean escaping = false;
for (int index = 0; index < line.length(); index++) {
char character = line.charAt(index);
if (escaping) {
current.append(character);
escaping = false;
} else if (character == '\\') {
escaping = true;
} else if (character == ';') {
parts.add(current.toString());
current.setLength(0);
} else {
current.append(character);
}
}
if (escaping) {
current.append('\\');
}
parts.add(current.toString());
return parts;
}
@Override
public String toString() {
return "StoredTask{" + id + ", " + title + ", " + status + ", p=" + priority + "}";
}
}
package com.seb4u.demo.taskflow.foundation;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public final class TaskTableFormatter {
public String asTable(List<StoredTask> tasks) {
String header = String.format("%-8s | %-26s | %-10s | %s", "ID", "TITLE", "STATUS", "PRIO");
String divider = "---------+----------------------------+------------+------";
String body = tasks.stream()
.sorted(Comparator.comparingInt(StoredTask::priority).thenComparing(StoredTask::title))
.map(task -> String.format("%-8s | %-26s | %-10s | %d",
task.id(), cut(task.title(), 26), task.status(), task.priority()))
.collect(Collectors.joining(System.lineSeparator()));
return header + System.lineSeparator() + divider + System.lineSeparator() + body;
}
private String cut(String value, int max) {
return value.length() <= max ? value : value.substring(0, max - 1) + "...";
}
}
package com.seb4u.demo.taskflow.foundation;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
class FileTaskRepositoryTest {
@TempDir
Path tempDir;
@Test
void savesAndLoadsTasks() throws Exception {
var repository = new FileTaskRepository();
var file = tempDir.resolve("tasks.csv");
var tasks = List.of(new StoredTask("T-1", "Test; CSV \\ prüfen", "TODO", 2));
repository.save(file, tasks);
assertEquals(1, repository.load(file).size());
assertEquals("Test; CSV \\ prüfen", repository.load(file).get(0).title());
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.seb4u.demo</groupId>
<artifactId>taskflow-java-masterclass</artifactId>
<version>1.1.0</version>
</parent>
<artifactId>taskflow-jakarta-api</artifactId>
<name>TaskFlow Jakarta API</name>
<packaging>war</packaging>
<properties><maven.compiler.release>17</maven.compiler.release></properties>
<dependencies>
<dependency>
<groupId>com.seb4u.demo</groupId>
<artifactId>taskflow-domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-web-api</artifactId>
<version>${jakarta.ee.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>taskflow-jakarta-api</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
package com.seb4u.demo.jakarta.taskflow;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
@ApplicationPath("/api")
public class TaskFlowApplication extends Application {
}
package com.seb4u.demo.jakarta.taskflow.api;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import java.time.Instant;
@Provider
public class TaskExceptionMapper implements ExceptionMapper<IllegalArgumentException> {
@Override
public Response toResponse(IllegalArgumentException exception) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("TASK_ERROR", exception.getMessage(), Instant.now()))
.build();
}
public record ErrorResponse(String code, String message, Instant timestamp) {
}
}
package com.seb4u.demo.jakarta.taskflow.api;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
public record TaskRequest(
@NotBlank String title,
@Min(1) @Max(5) int priority
) {
}
package com.seb4u.demo.jakarta.taskflow.api;
import com.seb4u.demo.jakarta.taskflow.service.JakartaTaskService;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.net.URI;
import java.util.List;
@Path("/tasks")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class TaskResource {
private JakartaTaskService service;
public TaskResource() {
}
@Inject
public TaskResource(JakartaTaskService service) {
this.service = service;
}
@GET
public List<TaskResponse> all() {
return service.findAll().stream().map(TaskResponse::from).toList();
}
@GET
@Path("/{id}")
public TaskResponse byId(@PathParam("id") String id) {
return service.findById(id)
.map(TaskResponse::from)
.orElseThrow(() -> new IllegalArgumentException("Task not found: " + id));
}
@POST
public Response create(@Valid TaskRequest request) {
var created = service.create(request.title(), request.priority());
return Response.created(URI.create("/api/tasks/" + created.id().value()))
.entity(TaskResponse.from(created))
.build();
}
@POST
@Path("/{id}/complete")
public TaskResponse complete(@PathParam("id") String id) {
return TaskResponse.from(service.complete(id));
}
}
package com.seb4u.demo.jakarta.taskflow.api;
import com.seb4u.demo.taskflow.domain.Task;
public record TaskResponse(String id, String title, String status, int priority) {
public static TaskResponse from(Task task) {
return new TaskResponse(task.id().value(), task.title(), task.status().name(), task.priority());
}
}
package com.seb4u.demo.jakarta.taskflow.service;
import com.seb4u.demo.taskflow.domain.Task;
import com.seb4u.demo.taskflow.domain.TaskId;
import com.seb4u.demo.taskflow.domain.TaskStatus;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@ApplicationScoped
public class JakartaTaskService {
private final Map<String, Task> tasks = new ConcurrentHashMap<>();
public List<Task> findAll() {
return tasks.values().stream()
.sorted(Comparator.comparingInt(Task::priority).thenComparing(Task::title))
.toList();
}
public Optional<Task> findById(String id) {
return Optional.ofNullable(tasks.get(id));
}
public Task create(String title, int priority) {
Task task = new Task(TaskId.random(), title, TaskStatus.TODO, priority);
tasks.put(task.id().value(), task);
return task;
}
public Task complete(String id) {
return tasks.compute(id, (key, existing) -> {
if (existing == null) {
throw new IllegalArgumentException("Task not found: " + id);
}
return existing.complete();
});
}
}
package com.seb4u.demo.jakarta.taskflow.service;
import com.seb4u.demo.taskflow.domain.TaskStatus;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class JakartaTaskServiceTest {
@Test
void createsAndCompletesTask() {
var service = new JakartaTaskService();
var created = service.create("Jakarta Resource schreiben", 2);
var completed = service.complete(created.id().value());
assertEquals(TaskStatus.DONE, completed.status());
assertTrue(service.findById(created.id().value()).isPresent());
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.seb4u.demo</groupId>
<artifactId>taskflow-java-masterclass</artifactId>
<version>1.1.0</version>
</parent>
<artifactId>taskflow-runtime</artifactId>
<name>TaskFlow Runtime - Java 21</name>
<properties><maven.compiler.release>21</maven.compiler.release></properties>
<dependencies>
<dependency>
<groupId>com.seb4u.demo</groupId>
<artifactId>taskflow-domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
package com.seb4u.demo.taskflow.runtime;
import com.seb4u.demo.taskflow.domain.TaskId;
public record AddTask(TaskId id, String title, int priority) implements TaskCommand {
}
package com.seb4u.demo.taskflow.runtime;
import com.seb4u.demo.taskflow.domain.TaskId;
public record CompleteTask(TaskId id) implements TaskCommand {
}
package com.seb4u.demo.taskflow.runtime;
import com.seb4u.demo.taskflow.domain.TaskId;
public record ReprioritizeTask(TaskId id, int priority) implements TaskCommand {
}
package com.seb4u.demo.taskflow.runtime;
import com.seb4u.demo.taskflow.domain.TaskId;
public sealed interface TaskCommand permits AddTask, CompleteTask, ReprioritizeTask {
TaskId id();
}
package com.seb4u.demo.taskflow.runtime;
import com.seb4u.demo.taskflow.domain.Task;
import com.seb4u.demo.taskflow.domain.TaskStatus;
public final class TaskCommandHandler {
public Task apply(Task current, TaskCommand command) {
return switch (command) {
case AddTask add -> new Task(add.id(), add.title(), TaskStatus.TODO, add.priority());
case CompleteTask complete when current.id().equals(complete.id()) -> current.complete();
case ReprioritizeTask reprioritize when current.id().equals(reprioritize.id()) -> current.withPriority(reprioritize.priority());
default -> current;
};
}
}
package com.seb4u.demo.taskflow.runtime;
import com.seb4u.demo.taskflow.domain.TaskEvent;
import java.util.List;
import java.util.SequencedCollection;
public final class TaskTimeline {
public List<TaskEvent> latestFirst(SequencedCollection<TaskEvent> events) {
return events.reversed().stream().toList();
}
}
package com.seb4u.demo.taskflow.runtime;
import com.seb4u.demo.taskflow.domain.Task;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Supplier;
public final class VirtualTaskProcessor {
public List<Task> process(List<Supplier<Task>> jobs) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<Task>> futures = jobs.stream()
.map(job -> executor.submit(job::get))
.toList();
List<Task> result = new ArrayList<>();
for (Future<Task> future : futures) {
result.add(await(future));
}
return List.copyOf(result);
}
}
private Task await(Future<Task> future) {
try {
return future.get();
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Task processing interrupted", exception);
} catch (ExecutionException exception) {
throw new IllegalStateException("Task processing failed", exception.getCause());
}
}
}
package com.seb4u.demo.taskflow.runtime;
import com.seb4u.demo.taskflow.domain.Task;
import com.seb4u.demo.taskflow.domain.TaskId;
import com.seb4u.demo.taskflow.domain.TaskStatus;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
class VirtualTaskProcessorTest {
@Test
void processesIndependentJobs() {
var processor = new VirtualTaskProcessor();
var result = processor.process(List.of(
() -> new Task(new TaskId("A"), "A", TaskStatus.TODO, 1),
() -> new Task(new TaskId("B"), "B", TaskStatus.TODO, 2)
));
assertEquals(2, result.size());
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.seb4u.demo</groupId>
<artifactId>taskflow-java-masterclass</artifactId>
<version>1.1.0</version>
</parent>
<artifactId>taskflow-spring-api</artifactId>
<name>TaskFlow Spring API</name>
<properties><maven.compiler.release>17</maven.compiler.release></properties>
<dependencies>
<dependency>
<groupId>com.seb4u.demo</groupId>
<artifactId>taskflow-domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
package com.seb4u.demo.spring.taskflow;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TaskflowSpringApplication {
public static void main(String[] args) {
SpringApplication.run(TaskflowSpringApplication.class, args);
}
}
package com.seb4u.demo.spring.taskflow.api;
import com.seb4u.demo.spring.taskflow.service.SpringTaskService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final SpringTaskService service;
public TaskController(SpringTaskService service) {
this.service = service;
}
@GetMapping
public List<TaskResponse> all() {
return service.findAll().stream().map(TaskResponse::from).toList();
}
@GetMapping("/{id}")
public TaskResponse byId(@PathVariable String id) {
return service.findById(id)
.map(TaskResponse::from)
.orElseThrow(() -> new IllegalArgumentException("Task not found: " + id));
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public TaskResponse create(@Valid @RequestBody TaskRequest request) {
return TaskResponse.from(service.create(request.title(), request.priority()));
}
@PostMapping("/{id}/complete")
public TaskResponse complete(@PathVariable String id) {
return TaskResponse.from(service.complete(id));
}
}
package com.seb4u.demo.spring.taskflow.api;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.Instant;
@RestControllerAdvice
public class TaskExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleIllegalArgument(IllegalArgumentException exception) {
return new ErrorResponse("TASK_ERROR", exception.getMessage(), Instant.now());
}
public record ErrorResponse(String code, String message, Instant timestamp) {
}
}
package com.seb4u.demo.spring.taskflow.api;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
public record TaskRequest(
@NotBlank String title,
@Min(1) @Max(5) int priority
) {
}
package com.seb4u.demo.spring.taskflow.api;
import com.seb4u.demo.taskflow.domain.Task;
public record TaskResponse(String id, String title, String status, int priority) {
public static TaskResponse from(Task task) {
return new TaskResponse(task.id().value(), task.title(), task.status().name(), task.priority());
}
}
package com.seb4u.demo.spring.taskflow.service;
import com.seb4u.demo.taskflow.domain.Task;
import com.seb4u.demo.taskflow.domain.TaskId;
import com.seb4u.demo.taskflow.domain.TaskStatus;
import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class SpringTaskService {
private final Map<String, Task> tasks = new ConcurrentHashMap<>();
public List<Task> findAll() {
return tasks.values().stream()
.sorted(Comparator.comparingInt(Task::priority).thenComparing(Task::title))
.toList();
}
public Optional<Task> findById(String id) {
return Optional.ofNullable(tasks.get(id));
}
public Task create(String title, int priority) {
Task task = new Task(TaskId.random(), title, TaskStatus.TODO, priority);
tasks.put(task.id().value(), task);
return task;
}
public Task complete(String id) {
return tasks.compute(id, (key, existing) -> {
if (existing == null) {
throw new IllegalArgumentException("Task not found: " + id);
}
return existing.complete();
});
}
}
package com.seb4u.demo.spring.taskflow.service;
import com.seb4u.demo.taskflow.domain.TaskStatus;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class SpringTaskServiceTest {
@Test
void createsAndCompletesTask() {
var service = new SpringTaskService();
var created = service.create("Spring Controller schreiben", 2);
var completed = service.complete(created.id().value());
assertEquals(TaskStatus.DONE, completed.status());
assertTrue(service.findById(created.id().value()).isPresent());
}
}