JAX https://jax.de/ Java, Architecture & Software Innovation Fri, 09 Oct 2026 09:28:02 +0000 de-DE hourly 1 https://wordpress.org/?v=7.1 Java-Code mit KI modernisieren https://jax.de/blog/core-java-jvm-languages/java-code-mit-ki-modernisieren/ Fri, 09 Oct 2026 07:25:14 +0000 https://jax.de/?p=210931 Noch vor einem Jahrzehnt beklagten sich viele Entwickler:innen darüber, dass sich Java nicht schnell genug weiterentwickelte.

The post Java-Code mit KI modernisieren appeared first on JAX.

]]>

Heute ist das Gegenteil der Fall: Java entwickelt sich in einem rasanten Sechsmonatsrhythmus weiter, und Anwendungen müssen Schritt halten, damit sich keine technischen Schulden anhäufen. In diesem Artikel betrachten wir die Vorteile und Herausforderungen der Modernisierung und zeigen, wie KI dabei helfen kann, welche Möglichkeiten sie eröffnet, wo Schwierigkeiten entstehen und wie wir damit umgehen können.

Fragen Sie Entwickler:innen bei einer User Group oder auf einer Konferenz, welche Java-Version ihre Anwendung nutzt, und Sie werden – je nachdem, wen Sie fragen – unterschiedliche Antworten hören. Manche murmeln widerwillig acht, elf oder siebzehn, andere nennen begeistert 21 oder 25. Diese kurze Umfrage auf mehreren jüngeren Konferenzen zeigt: Bei den eingesetzten Versionen ist praktisch alles vertreten.

Vorteile der Java-Modernisierung

Wenn unsere Anwendungen möglichst nah an aktuellen Java-Versionen bleiben, hat das viele Vorteile:

  • Wir können neuere Features nutzen, die den Code ausdrucksstärker, weniger fehleranfällig sowie einfacher zu entwerfen, zu lesen und zu warten machen.
  • Wir können Features wie Virtual Threads einführen, die erhebliche architektonische Auswirkungen auf die Skalierung einer Anwendung haben können.
  • Wir sind für Entwickler:innen attraktiver – die meisten von uns arbeiten lieber mit modernem als mit veraltetem Code.
  • Wir können sicherheitsrelevante Updates leichter zeitnah übernehmen.
  • Um ein Framework oder eine Library zu aktualisieren, müssen wir möglicherweise zunächst auf die dafür erforderliche Basisversion wechseln.
  • Wir senken die Kosten für Upgrades, wenn wir mit den Versionswechseln Schritt halten und nicht zurückfallen.

Können wir nicht einfach die JVM-Version aktualisieren und es dabei belassen? Wenn wir diesen einfachen Weg wählen, schöpfen wir die Vorteile einer neueren Java-Version nicht vollständig aus. Außerdem könnten einige Methoden inzwischen deprecated sein, und wir möchten diese unschönen Deprecation Warnings nicht im Code sehen – insbesondere dann nicht, wenn wir die bewährte Praxis befolgen, Warnungen als Fehler zu behandeln. Mit neueren Versionen Schritt zu halten, ist grundsätzlich sinnvoll. Warum also zögern wir dennoch?

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Herausforderungen der Modernisierung

Die Modernisierung von Anwendungen bringt beinahe ebenso viele Herausforderungen wie Vorteile mit sich. Einige davon kennen Sie möglicherweise aus Ihren Teams:

  • Die Modernisierung kostet Zeit.
  • Sie erfordert Aufwand und Geld.
  • Entwickler:innen kennen eventuell viele der möglichen Verbesserungen nicht.
  • Wer bei den Änderungen an Sprache oder JDK auf dem Laufenden geblieben ist, ist womöglich mit anderen Aufgaben beschäftigt und steht für das Upgrade nicht zur Verfügung – das lässt sich personell nicht beliebig skalieren.
  • Wir müssen große Mengen Code aktualisieren.
  • Wir möchten mehrere Projekte oder eine große Zahl von Code-Repositorys aktualisieren.
  • Wir könnten das Verhalten des Codes versehentlich nachteilig verändern.

Modernisierung ist eine Teilmenge des Refactorings, das häufig äußerst nützlich ist. Dennoch ist es schwierig, diejenigen, die den Aufwand finanzieren, von seinem Wert zu überzeugen, da der Code danach nichts anderes tut. Denn Refactoring verändert nicht, was der Code tut, sondern wie er es tut.

Angesichts der jüngsten Begeisterung für KI liegt die Frage nahe, ob sie nicht die meisten der genannten Herausforderungen bewältigen kann. Könnten wir so die Vorteile nutzen, ohne die damit verbundenen Probleme in Kauf nehmen zu müssen?

Deterministische und nichtdeterministische Tools

Für die Modernisierung von Java-Code stehen uns zwei Optionen zur Verfügung, die sich nicht gegenseitig ausschließen: deterministische und nichtdeterministische Tools. Beide haben Vor- und Nachteile; im Allgemeinen profitieren wir davon, sie miteinander zu kombinieren.

OpenRewrite ist ein Beispiel für ein deterministisches Tool. Es stellt zahlreiche sogenannte Recipes bereit, bei denen es sich im Wesentlichen um Transformationen handelt. Für die Modernisierung würden Sie ein Recipe verwenden, das Code von einer der vielen älteren Java-Versionen – beispielsweise Version 7 – auf eine der LTS-Versionen 8, 11, 17, 21 oder 25 überführen kann. Wenn Sie OpenRewrite etwa bitten, von Version 11 auf 25 zu modernisieren, transformiert es den Code schrittweise auf 17, anschließend auf 21 und schließlich auf 25. Dabei nutzt es Recipes, die den Code jeweils von einer LTS-Version auf die nächste überführen.

Ich bezeichne das als deterministisch, weil es keine Überraschungen gibt: Für einen bestimmten Code führt das Tool stets exakt dieselbe Transformation aus. Voraussetzung ist, dass wir jeweils mit dem ursprünglichen Code beginnen und dasselbe Recipe anwenden – unabhängig davon, wie oft wir es ausführen. Diese Vorhersagbarkeit ist nützlich.

KI gehört dagegen zur Kategorie der nichtdeterministischen Tools. Sie können eines von vielen KI-Tools bitten, Ihren Code auf eine neue Java-Version zu modernisieren. Wie wir alle erfahren haben, ist KI sehr leistungsfähig, benötigt aber erhebliche Anleitung und Kontrolle, wenn sie Code erstellt oder transformiert. Ich bezeichne sie als nichtdeterministisch, weil der generierte Code jedes Mal anders ausfallen kann, wenn Sie ein bestimmtes Modell um die Modernisierung eines Codeabschnitts bitten. Sie sollten daher nicht erwarten, dass es bei verschiedenen Durchläufen exakt denselben Code erzeugt.

Das liegt in der Natur von LLMs: Sie nutzen Pattern Matching und Inferenz, um zu bestimmen, wie sie eine Aufgabe bearbeiten. Sie folgen dabei keiner vorhersehbaren Abfolge. Entsprechend kann das Ergebnis bei jedem Durchlauf anders ausfallen.

Damit stellt sich die Frage, ob wir von den deterministischen und nichtdeterministischen Optionen einer den Vorzug geben sollten. Wegen der jeweiligen Vor- und Nachteile fällt eine eindeutige Entscheidung jedoch schwer (Tabelle 1).

deterministisch nichtdeterministisch
vorhersagbar ✓ x
konsistent ✓ x
ohne umfangreiche Kontrolle einsetzbar ✓ x
Fähigkeiten begrenzt unbegrenzt
JDK-bezogene Updates ✓ ✓
Sprachänderungen ~ ✓
Paradigmenwechsel x ✓

✓ – ja, x – nein, ~ – möglicherweise mit individuellem Aufwand umsetzbar

Tabelle 1: Vor- und Nachteile deterministischer und nichtdeterministischer Tools

Die Abwägung zwischen beiden Optionen ist klar: Die eine ist vorhersagbar und konsistent, aber in ihren Möglichkeiten begrenzt. Die andere ist sehr leistungsfähig, kann jedoch aus dem Ruder laufen. Wegen ihrer jeweiligen Vorteile empfehle ich daher häufig, beide zu kombinieren.

In diesem Artikel konzentrieren wir uns auf die zweite Option: den Einsatz von KI zur Modernisierung von Java-Code.

Java-Code mit KI modernisieren

Sie können ein beliebiges LLM verwenden; probieren Sie dabei unbedingt das Modell aus, das Sie normalerweise oder am häufigsten einsetzen. In diesem Artikel verwenden wir Claude Code [1] über die Kommandozeile.

Zunächst sehen wir uns ein kleines Beispielprojekt an. Wir besprechen einige Transformationen, die wir im Zuge der Modernisierung vornehmen möchten. Anschließend modernisieren wir das Projekt mit Claude Code, erörtern das Ergebnis und entwickeln die Lösung anhand unserer Beobachtungen schrittweise weiter.

Wenn Sie das Beispiel auf Ihrem System durcharbeiten und KI zur Modernisierung Ihres eigenen Codes einsetzen, sollten Sie sowohl ihre Leistungsfähigkeit als auch ihre Unvorhersagbarkeit im Blick behalten. Das sind zwei Seiten derselben Medaille – das eine gibt es nicht ohne das andere.

Risiken von Refactoring und Modernisierung mindern

Eine Transformation, die die Implementierung verbessert, darf das Verhalten des Codes nicht verändern. Eines der Risiken beim Refactoring – und Modernisierung ist eine Form davon – besteht darin, dass sich der Code anschließend anders verhält als ursprünglich vorgesehen. Um dieses Risiko zu mindern, benötigen wir gute Feedback-Loops: Tests, die bestätigen, dass das zuvor funktionierende Verhalten auch nach der Codeänderung erhalten bleibt.

Idealerweise verfügen wir für den zu refaktorierenden Code über automatisierte Unit-Tests. Ist das weit von der Realität entfernt, helfen auch andere Arten automatisierter Tests, etwa Functional-Tests, Acceptance-Tests oder Integrationstests. Fehlen solche Tests vollständig, müssen wir möglicherweise auf manuelle Tests zurückgreifen. Entscheidend ist, überprüfen zu können, dass die geänderte Implementierung das beabsichtigte Verhalten nicht beeinträchtigt.

Selbst bei vorhandenen automatisierten Tests möchte ich sicherstellen, dass sie den refaktorierten Code tatsächlich sinnvoll abdecken. Dazu könnten wir den Code vorübergehend so verändern, dass er ein anderes Ergebnis liefert, und anschließend prüfen, ob mindestens einer der relevanten Tests fehlschlägt. So lässt sich feststellen, ob die Tests eine Regression tatsächlich erkennen, und wir gewinnen die nötige Sicherheit.

Ein Beispielprogramm für die Modernisierung

Arbeiten wir mit einem kleinen Beispiel, bei dem wir Claude Code einsetzen und den Code modernisieren können. Der Code basiert auf den in Java 7 verfügbaren Features, wurde jedoch mit Java 8 gebaut. Wir aktualisieren ihn auf Java 25, die derzeit aktuelle LTS-Version, um die modernen Features der Sprache nutzen zu können.

Unser Beispiel umfasst neben der Maven-Konfigurationsdatei pom.xml nur zwei Source-Dateien und eine Testdatei. Die kleine Anwendung ist aufgebaut wie in Listing 1, den Test sehen Sie in Listing 2.

Listing 1
.
|____pom.xml
|____src
| |____test
| | |____java
| | | |____com
| | | | |____agiledeveloper
| | | | | |____example
| | | | | | |____NamesTransformerTest.java
| |____main
| | |____java
| | | |____com
| | | | |____agiledeveloper
| | | | | |____example
| | | | | | |____NamesTransformer.java
| | | | | | |____Person.java
Listing 2
package com.agiledeveloper.example;

import java.util.Arrays;
import java.util.ArrayList;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class NamesTransformerTest {
  private NamesTransformer namesTransformer;

  @BeforeEach
  void init() {
    namesTransformer = new NamesTransformer();
  }

  @Test
  void testConvertNamesToUpperCaseWhenThereAreNoNames() {
    assertEquals(new ArrayList<>(), namesTransformer.toUpperCase(new ArrayList<String>() {}));
  }

  @Test
  void testConvertNamesToUpperCaseWhenThereIsOneName() {
    assertEquals(new ArrayList<String>() {{ add("SAM"); }}, namesTransformer.toUpperCase(new ArrayList<String>() {{ add("Sam"); }}));
  }

  @Test
  void testConvertNamesToUpperCaseWhenThereAreTwoName() {
    assertEquals(new ArrayList<String>() {{ add("SAM"); add("JILL"); }}, namesTransformer.toUpperCase(new ArrayList<String>() {{ add("Sam"); add("JiLl"); }}));
  }

  @Test
  void testConvertNamesToUpperCaseWhenThereAreThreeName() {
    assertEquals(new ArrayList<String>() {{ add("SAM"); add("JILL"); add("PAULA"); }}, namesTransformer.toUpperCase(new ArrayList<String>() {{ add("Sam"); add("JiLl"); add("Paula"); }}));
  }

  @Test
  void testGetFirstAndLastNamesWhenThereAreNoNames() {
    assertEquals(Arrays.asList(), namesTransformer.getEndNames(Arrays.asList()));
  }

  @Test
  void testGetFirstAndLastNamesWhenThereIsOneName() {
    assertEquals(Arrays.asList("Paul"), namesTransformer.getEndNames(Arrays.asList("Paul")));
  }

  @Test
  void testGetFirstAndLastNamesWhenThereAreTwoName() {
    assertEquals(Arrays.asList("Paul", "Grace"), namesTransformer.getEndNames(Arrays.asList("Paul", "Grace")));
  }

  @Test
  void testGetFirstAndLastNamesWhenThereAreThreeName() {
    assertEquals(Arrays.asList("Paul", "Pete"), namesTransformer.getEndNames(Arrays.asList("Paul", "Grace", "Pete")));
  }

  @Test
  void testGetFirstAndLastNamesWhenThereAreFourName() {
    assertEquals(Arrays.asList("Paul", "Mary"), namesTransformer.getEndNames(Arrays.asList("Paul", "Mike", "Bob", "Mary")));
  }

  @Test
  void testGreetSam() {
    String message = "Hello Sam,\nIt is wonderful seeing you.\nHave a great day!";

    assertEquals(message, namesTransformer.greetPerson(new Person("Sam", 12)));
  }

  @Test
  void testGreetSara() {
    String message = "Hello Sara,\nIt is wonderful seeing you.\nHave a great day!";

    assertEquals(message, namesTransformer.greetPerson(new Person("Sara", 21)));
  }
}

Wir haben einige Tests für drei Methoden der Klasse NamesTransformer. Die Tests der Methode toUpperCase() überprüfen das Verhalten bei unterschiedlich vielen Elementen in der als Argument übergebenen Liste, beginnend mit einer leeren Liste. Entsprechend prüfen die Tests für getEndNames() das Verhalten bei verschiedenen Anzahlen übergebener Elemente. Die Tests für greetPerson() überprüfen schließlich den erwarteten Text, wenn die Methode mit unterschiedlichen Argumenten aufgerufen wird.

Die Klasse Person (Listing 3) ist ein Plain Old Java Object (POJO). Sie besitzt zwei immutable Felder, name und age, sowie Getter für den Zugriff auf beide. Der Konstruktor setzt lediglich diese beiden Felder. Es handelt sich schlicht um recht ausführlichen Code.

Listing 3
package com.agiledeveloper.example;

public class Person {
  private final String name;
  private final int age;

  public Person(String theName, int theAge) {
    name = theName;
    age = theAge;
  }

  public String getName() { return name; }
  public int getAge() { return age; }
}

Die Klasse NamesTransformer enthält drei Methoden:

  • toUpperCase() nimmt eine Liste mit Namen entgegen und gibt diese Namen in Großbuchstaben zurück.
  • getEndNames() nimmt eine Liste mit Namen entgegen und gibt (sofern vorhanden) das erste und das letzte Element zurück.
  • greetPerson() gibt einen mehrzeiligen String mit einer freundlichen Nachricht zurück, die eine bestimmte Person mit ihrem name anspricht.

Der Code (Listing 4) verwendet einen imperativen Stil, herkömmliche if- und else-Statements sowie den Operator +=, um mehrere String-Zeilen zu verketten – eine ziemlich ausführliche und unerwünschte Konstruktion, die wir nicht gerne vorzeigen.

Listing 4
package com.agiledeveloper.example;

import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;
import java.util.Locale;

public class NamesTransformer {
  public List<String> toUpperCase(List<String> names) {
    ArrayList<String> result = new ArrayList<>();

    for(int i = 0; i < names.size(); i++) {
      result.add(names.get(i).toUpperCase(new Locale("en")));
    }

    return result;
  }

  public List<String> getEndNames(List<String> names) {
    if(names.size() == 0) {
      return Arrays.asList();
    } else {
      if(names.size() == 1) {
        return Arrays.asList(names.get(0));
      } else {
        return Arrays.asList(names.get(0), names.get(names.size() - 1));
      }
    }
  }

  public String greetPerson(Person person) {
    String message = "";
    message += "Hello ";
    message += person.getName();
    message += ",\n";
    message += "It is wonderful seeing you.\n";
    message += "Have a great day!";

    return message;
  }
}

Zum Schluss betrachten wir noch die Maven-Datei pom.xml (Listing 5). Sie zeigt, dass der Code für Java 8 vorgesehen ist, und bindet JUnit ein, um die Ausführung der Tests zu ermöglichen.

Listing 5
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.agiledeveloper.example</groupId>
  <artifactId>exercise</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>exercise</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

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

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.junit</groupId>
        <artifactId>junit-bom</artifactId>
        <version>5.11.0</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- Optionally: parameterized tests support -->
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-params</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.4.0</version>
        </plugin>
        <!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.3.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.13.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>3.3.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-jar-plugin</artifactId>
          <version>3.4.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>3.1.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>3.1.2</version>
        </plugin>
        <!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
        <plugin>
          <artifactId>maven-site-plugin</artifactId>
          <version>3.12.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-project-info-reports-plugin</artifactId>
          <version>3.6.1</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

NEUES AUS DER JAVA-BACKEND-WELT

Java-Backend-Track entdecken

 

Vor der Modernisierung

Im ersten Schritt sollten wir sicherstellen, dass die Tests erfolgreich durchlaufen. Grundsätzlich sollte der Code außerdem in einer Versionsverwaltung wie git committed sein, bevor wir Änderungen vornehmen. Falls etwas schiefgeht, können wir so schnell zur funktionierenden Version zurückkehren, ohne viel Zeit zu verschwenden. In Listing 6 sehen wir das Ergebnis von mvn test.

Listing 6
...
[INFO] ------------------------------------- -----
[INFO]  T E S T S
[INFO] -------------------------------------------
[INFO] Running com.agiledeveloper.example.NamesTransformerTest
[INFO] Tests run: 11, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.019 s -- in com.agiledeveloper.example.NamesTransformerTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 11, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] -------------------------------------------
[INFO] BUILD SUCCESS
...

Die Testausführung zeigt, dass 11 Tests erfolgreich waren und es keine fehlgeschlagenen, fehlerhaften oder übersprungenen Tests gab. Das ist eine gute Ausgangslage für unsere Modernisierung.

Skills erstellen

KI ist ein zweischneidiges Schwert: Sie ist sehr leistungsfähig, aber äußerst unvorhersagbar. Nur weil sie einen Codeabschnitt einmal oder sogar mehrmals auf eine bestimmte Weise transformiert hat, gibt es keine Garantie, dass sie es jedes Mal konsistent tut. Der Nichtdeterminismus kann sich auf höchst frustrierende Weise bemerkbar machen. Außerdem möchten wir die Wahrscheinlichkeit minimieren, dass KI eher unerwünschten Code erzeugt – etwa eine funktionale Pipeline, die einen außerhalb der Pipeline liegenden gemeinsamen State verändert. Gibt es modernere Alternativen für eine Methode, soll die KI relevanten Code beispielsweise in diese modernen Entsprechungen überführen. Obwohl KI nichtdeterministisch ist, möchten wir ihr Anweisungen, Empfehlungen und unsere Präferenzen mitgeben können, um sie auf den gewünschten Weg zu lenken. Hier kommen Skill-Dateien ins Spiel.

Eine Skill-Datei namens SKILL.md enthält Richtlinien, Empfehlungen sowie Gebote und Verbote, die KI bei der Ausführung Ihrer Befehle berücksichtigen soll. Bei Claude Code liegen Skill-Dateien im Unterverzeichnis .claude/skills unterhalb des Top-Level-Verzeichnisses Ihres Projekts. In der Regel erstellen wir mehrere Skill-Dateien, die sich jeweils auf ein zentrales Anliegen oder Themengebiet konzentrieren. Beispielsweise können Sie der KI in einer Skill-Datei Ihre Präferenzen und Best Practices für JUnit-Tests erläutern. Eine weitere könnte Ihre Vorlieben und Abneigungen beim funktionalen Programmierstil beschreiben, eine andere Clean-Code-Praktiken, Richtlinien für Spring-bezogenen Code und so weiter.

Für unser Beispiel erstellen wir eine einzige Skill-Datei: .claude/skills/java-coding-practices/SKILL.md. In dieser Datei halten wir einige Empfehlungen fest, die die KI befolgen soll (Listing 7). Diese Skill-Datei kratzt lediglich an der Oberfläche und ist nicht vollständig. In einem realen Szenario würden wir sie mit mehreren Beispielen deutlich erweitern. Außerdem hätten wir mehrere Skill-Dateien für weitere Aspekte, etwa für funktionalen Programmierstil, den sinnvollen Einsatz von Optional, die Verwendung des Stream API oder den geeigneten Einsatz von Virtual Threads.

Listing 7
---
name: java—coding-practices
description: Use this when creating Java code
---

# Java Coding Practices and Preferences
Use the following Java Coding Practices and Preference when generating or transforming Java code.

##  Java Version
Prefer using JDK and Java version 25. When deciding language features, choose the latest features where possible. When deciding JDK functions, prefer latest methods and avoid using any deprecated methods.

* Avoid using any preview features. Only use features that are available for general use.

##  Coding Conventions
* Avoid single letter variable names. Prefer descriptive yet short name for variables.
* Avoid single letter parameter names, including lambda parameters. Use domain specific names instead.
* When creating for and if, use {} even if there is only one statement in the body.
* Give a blank line, where necessary, to improve readability of code.
* Avoid long functions.
* Prefer immutability where possible.
* Where it makes sense and possible, prefer functional style over imperative style of programming. Where possible, prefer iterating using stream rather than for or while.
* Use modern alternatives to methods in the JDK. For example, where it makes sense, use the SequencedCollection methods instead of the traditional methods.

Wenn Sie KI zum Generieren neuen Codes oder zum Refactoring vorhandenen Codes einsetzen, kann das Ergebnis unerwünscht oder unzureichend ausfallen. Nutzen Sie solche Fälle als Gelegenheit, die Skill-Dateien weiterzuentwickeln. Ergänzen Sie darin Beispiele für die betreffende Situation und zeigen Sie, welchen Code die KI stattdessen erzeugen oder in welche Form sie ihn transformieren soll. Damit erhöhen Sie die Wahrscheinlichkeit, dass die KI unter den möglichen Lösungen die von Ihnen bevorzugte wählt.

Da sich die Datei .claude/skills/java-coding-practices/SKILL.md nun im Projekt befindet, starten wir Claude Code mit dem Befehl claude und erteilen folgenden Auftrag:

> Modernize the code in this project and do not modify anything under the test directory or its subdirectories.

Beim Refactoring ist es äußerst wichtig, die Tests nicht zu verändern. Unser Ziel ist hier jedoch, die gesamte Anwendung zu modernisieren; deshalb sollen später auch die Tests moderne Features nutzen. Ändern wir Source und Tests gleichzeitig, können wir allerdings nicht mehr sicher sein, dass sich das Verhalten des Codes nicht verändert hat. Modernisieren Sie daher zunächst den Source Code, ohne die Tests anzutasten. Sind diese Arbeiten abgeschlossen und laufen alle Tests erfolgreich, folgt der nächste Schritt: Lassen Sie den Source Code unverändert und modernisieren Sie die Tests.

Wir haben Claude um eine Modernisierung gebeten, aber keine Zielversion genannt. Wir könnten sie ausdrücklich angeben. Da wir die Version jedoch bereits in der Skill-Datei festgelegt haben, können wir darauf verzichten. Vergewissern Sie sich trotzdem, dass Claude tatsächlich auf die gewünschte Version modernisiert.

Wenn Claude mit der angeforderten Aufgabe beginnt, sehen Sie etwa Folgendes:

skill(java-coding-practices)
Successfully loaded skill

Das bestätigt, dass Claude den von uns bereitgestellten Skill gefunden hat und verwendet.

Denken Sie daran, dass das bei Ihnen sichtbare Refactoring-Ergebnis von dem hier gezeigten abweichen kann – ein weiterer Hinweis darauf, dass KI nichtdeterministisch und inkonsistent ist. Sobald Claude die Aufgabe abgeschlossen hat, gibt es die Meldung in Listing 8 aus.

Listing 8
All 11 tests pass. Summary of the modernization (test directory untouched):

pom.xml — bumped maven.compiler.release from 8 to 25.

Person.java — converted the hand-written immutable class to a record, since it was just a final-field data holder with a constructor and two accessors. The canonical name()/age() accessors replace getName()/getAge().

NamesTransformer.java:
- toUpperCase: replaced the manual indexed loop over an ArrayList with a stream map().toList(); swapped the deprecated new Locale("en") constructor for Locale.of("en").
- getEndNames: replaced nested if/Arrays.asList branching with SequencedCollection's getFirst()/getLast() and List.of(...).
- greetPerson: replaced repeated += string concatenation with a text block and .formatted(...).

Ran mvn test against JDK 25 — build succeeds, all 11 existing tests pass unchanged.

Wenn Sie wie ich vorsichtig optimistisch sind, sollten Sie mvn test außerhalb von Claude Code auf der Kommandozeile ausführen und prüfen, ob tatsächlich alle Tests erfolgreich sind. Ein kurzer git diff sollte außerdem bestätigen, dass Claude weder Dateien im Testverzeichnis noch in dessen Unterverzeichnissen verändert hat.

Sehen wir uns die Änderungen an, die Claude vorgenommen hat. Zunächst änderte es die Datei pom.xml wie folgt:

<maven.compiler.release>8</maven.compiler.release>

in

<maven.compiler.release>25</maven.compiler.release>

Eine sinnvolle Änderung: Da wir Version 25 als Ziel angegeben haben, passte Claude die Maven-Build-Datei an die richtige Version an.

Im Verzeichnis src sehen wir, dass die Klasse Person in einen record umgewandelt wurde:

package com.agiledeveloper.example;
public record Person(String name, int age) {
}

Eine Folge dieser Änderung: Anstelle der bisherigen Methoden getName() und getAge() der Klasse Person gibt es jetzt die Methoden name() und age(). Deshalb müssen die Aufrufe von Methoden der Klasse Person in der Klasse NamesTransformer entsprechend angepasst werden. Behalten wir das im Blick, während wir uns diese Klasse genauer ansehen. Gehen wir die einzelnen Funktionen der Klasse NamesTransformer durch und betrachten, wie sie transformiert beziehungsweise modernisiert wurden.

Die Methode toUpperCase() war imperativ implementiert und nutzte die herkömmliche for-Schleife. Hier ist die modernisierte Fassung:

public List<String> toUpperCase(List<String> names) {
  return names.stream()
    .map(name -> name.toUpperCase(Locale.of("en")))
    .toList();
}

In der Skill-Datei haben wir unsere Präferenz für einen funktionalen Stil mit dem Stream API angegeben. Zusätzlich zur Umstellung auf eine funktionale Pipeline hat Claude den inzwischen deprecated Konstruktoraufruf von Locale durch einen Aufruf der Methode of() ersetzt. Statt des älteren Collectors.toList() oder Collectors.toUnmodifiableList() verwendet es außerdem die vergleichsweise neue Methode toList() von Stream. Das entspricht unserer Präferenz für Immutability: Die Methode toList() von Stream gibt eine immutable Collection zurück. Insgesamt wurde diese Methode sehr gut modernisiert.

Betrachten wir als Nächstes die Transformation der Methode greetPerson(). In der ursprünglichen Version wurde der Operator += wiederholt aufgerufen; der Code war ausführlich und wenig ansprechend. Das Java-Feature Text-Blocks ist ein geeigneter Ersatz. Der transformierte Code zeigt, dass Claude diese Modernisierung genau getroffen hat:

public String greetPerson(Person person) {
  return """
    Hello %s,
    It is wonderful seeing you.
    Have a great day!""".formatted(person.name());
}

Zusätzlich zum Text-Block ruft der Code die neue Methode formatted() auf – ebenfalls gut umgesetzt.

Sehen wir uns nun die Methode getEndNames() an. Die ursprüngliche Implementierung von getEndNames() hatte drei Probleme:

  1. Sie verwendete verschachtelte if-else-Statements. Verschachtelung erhöht die Komplexität des Codes, erschwert sein Verständnis und ist fehleranfällig.
  2. Sie verwendete das alte Arrays.asList(), das eine Liste erzeugt, deren Elemente ersetzt werden können. Eine immutable Liste wäre ideal; die Methode List.of() ist eine deutlich bessere Alternative zu Arrays.asList().
  3. Der Aufruf names.get(0) zum Abrufen des ersten Elements und names.get(names.size() – 1) zum Abrufen des letzten Elements ist uns bestens vertraut. Es muss jedoch eine bessere, weniger ausführliche und ausdrucksstärkere Möglichkeit geben. Die in Java 21 eingeführte SequencedCollection bietet mit getFirst() und getLast() elegantere Alternativen zu diesen beiden Aufrufen.

In Listing 9 sehen wir, wie Claude diese Methode modernisiert. Das Ergebnis ist durchwachsen.

Listing 9
public List<String> getEndNames(List<String> names) {
  if (names.isEmpty()) {
    return List.of();
  }

  if (names.size() == 1) {
    return List.of(names.getFirst());
  }

  return List.of(names.getFirst(), names.getLast());
}

Zunächst verwendet Claude anstelle der älteren ausführlichen Methodenaufrufe die Methoden getFirst() und getLast() – das ist eine gute Lösung. Außerdem hat es Arrays.asList() elegant durch die moderne Methode List.of() ersetzt. Gut gemacht, Claude!

Claude hat das verschachtelte if-else-Statement aufgelöst und dabei berücksichtigt, dass nach einem return kein else nötig ist. Dennoch trifft die Transformation meine Vorstellungen noch nicht ganz. Es wäre deutlich besser, das moderne Pattern Matching mit switch zu verwenden. Denken Sie an die frühere Empfehlung: Nutzen Sie jede beobachtete Unzulänglichkeit als Gelegenheit, die Skill-Datei oder Skill-Dateien zu aktualisieren.

Enterprise AI & Agentic Systems

Enterprise AI-Track entdecken

 

Skills weiterentwickeln

Wir können die Skill-Datei um eine ausdrückliche Handlungsanweisung ergänzen – Pattern-Matching-Syntax verwenden – und beobachten, wie Claude damit umgeht. Bearbeiten wir die Datei SKILL.md wie folgt:

...
* Use modern alternatives to methods in the JDK. For example, where it makes sense, use the SequencedCollection methods instead of the traditional methods.
* Where possible, use the pattern matching with switch instead of multiple if or if-else statements.

Nachdem wir die Datei .claude/skills/java-coding-practices/SKILL.md wie oben beschrieben aktualisiert haben, starten wir Claude Code erneut und erteilen folgenden Auftrag:

> Revisit the code for any further improvements for modernizations, again do not modify anything under the test directory.

Listing 10 zeigt Claudes Antwort auf diesen Befehl mit der aktualisierten SKILL.md.

Listing 10
Person.java and the test file needed no changes — they're already current. In NamesTransformer.java, getEndNames was still a three-way if/early-return cascade; per your team's practice of preferring switch over multiple if/if-else, I converted it to a switch expression on names.size():

public List<String> getEndNames(List<String> names) {
  return switch (names.size()) {
    case 0 -> List.of();
    case 1 -> List.of(names.getFirst());
    default -> List.of(names.getFirst(), names.getLast());
  };
}

Same behavior, more concise, and consistent with the rest of the class's style. toUpperCase and greetPerson already use current idioms (Locale.of, toList(), text blocks) and didn't need changes. Ran mvn test — all tests still pass; I didn't touch anything under src/test.

Das sieht deutlich besser aus und entspricht eher meinen Vorstellungen. Ich würde mvn test erneut auf der Kommandozeile ausführen und prüfen, ob alle Tests erfolgreich sind.

Nachdem wir den Code committed haben, können wir Claude bitten, die Tests zu modernisieren, ohne Code im Verzeichnis src/main zu verändern. Wie bereits erwähnt, müssen Änderungen an den Tests unbedingt in einem separaten Schritt von den Änderungen am Source Code erfolgen.

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Zusammenfassung

KI ist ein äußerst leistungsfähiges Tool und sehr nützlich, um Java-Code – oder auch Code in anderen Programmiersprachen – zu modernisieren. Ihr größter Vorteil liegt in der Geschwindigkeit, mit der sich diese Aufgabe mit geringem Aufwand erledigen lässt. Der Haken dabei: Sie ist nichtdeterministisch, unvorhersagbar und inkonsistent. Deshalb müssen wir geeignete Skill-Dateien bereitstellen, um sie anzuleiten und auf Kurs zu halten.

Außerdem benötigen wir eine wirklich gute Testbasis, um sicherzustellen, dass die Transformation des Codes das beabsichtigte Verhalten nicht verändert. Ebenso müssen wir den Code reviewen, um zu prüfen, ob die Transformation unseren Erwartungen entspricht. Jede verbesserungswürdige Stelle bietet dabei die Gelegenheit, eine oder mehrere Skill-Dateien weiterzuentwickeln und die Modernisierung iterativ fortzusetzen.

Links & Literatur

[1]

https://code.claude.com/docs/en/quickstart

The post Java-Code mit KI modernisieren appeared first on JAX.

]]>
Java 27: Die wichtigsten Neuerungen https://jax.de/blog/java-27-neuerungen/ Fri, 02 Oct 2026 09:01:19 +0000 https://jax.de/?p=210911 Java 27 erschien im September 2026 und bringt diesmal keine großen neuen Sprachfeatures. Trotzdem enthält das Release Änderungen, die sich unmittelbar auf bestehende Anwendungen auswirken können – teilweise sogar ganz ohne Änderungen am Quellcode. Neue Defaults, Sicherheitsfunktionen und weiterentwickelte APIs machen das Update deshalb auch für bestehende Java-Anwendungen relevant.

The post Java 27: Die wichtigsten Neuerungen appeared first on JAX.

]]>
Vier JEPs bringen tatsächlich neue beziehungsweise erstmals standardmäßig aktivierte Funktionalität: Compact Object Headers werden zum Default, G1 wird unabhängig von der Umgebung zum Standard-Garbage-Collector, TLS 1.3 erhält einen hybriden Post-Quantum-Schlüsselaustausch und der Java Flight Recorder schwärzt sensible Informationen bereits während der Aufzeichnung. Die fünf weiteren JEPs sind Wiedervorlagen bereits bekannter Preview- oder Inkubator-Features. Die Liste der umgesetzten JEPs ist daher diesmal nicht allzu lang [1]:

  • 523: Make G1 the Default Garbage Collector in All Environments

  • 527: Post-Quantum Hybrid Key Exchange for TLS 1.3

  • 531: Lazy Constants (Third Preview)

  • 532: Primitive Types in Patterns, instanceof, and switch (Fifth Preview)

  • 533: Structured Concurrency (Seventh Preview)

  • 534: Compact Object Headers by Default

  • 536: JFR In-Process Data Redaction

  • 537: Vector API (Twelfth Incubator)

  • 538: PEM Encodings of Cryptographic Objects (Third Preview)

Trotzdem sind da einige spannende Themen und Entwicklungen dabei. Und wie üblich finden sich auch zahlreiche kleinere Änderungen in APIs und Laufzeitumgebung, die durchaus einen Blick wert sind. Und nur weil das Release klein erscheint, bedeutet „ruhiger“ nicht „unbedeutend“.

Vielmehr passieren unter der Haube einige Vorbereitungen für die nächste große Baustelle: Project Valhalla. Denn direkt nach dem Feature-Freeze zum OpenJDK 27 im Juni 2026 wurden die ersten großen Änderungen zu Value Classes als Vorbote für das OpenJDK 28 in das Haupt-Git-Repo gemerged [2]. Das wird die größte Veränderung des Java-Typsystems seit Generics.

Die Releases im Jahr 2026 sind also Teil einer Übergangsphase – die Plattform wird konzeptionell, technisch und sicherheitsseitig auf die nächsten großen Änderungen vorbereitet. Das OpenJDK 27 ist kein spektakuläres Featurerelease, aber ein strategisches. Es konsolidiert Performance, Nebenläufigkeit, Sicherheitsmodell und bereitet so auf Value Classes, stärkere Encapsulation, strukturelle Pattern-Konzepte und KI-Workloads vor. Mit anderen Worten, es ist Teil einer Evolution, die das Fundament der Plattform modernisiert – ohne ihre Stabilität und vor allem die Abwärtskompatibilität zu gefährden.

Compact Object Headers werden Standard

Jedes Java-Objekt besitzt neben seinen eigentlichen Daten einen Header. Die HotSpot VM speichert darin unter anderem Informationen über die Klasse des Objekts, dessen Identity Hash Code, Synchronisation und Garbage Collection. Bei einer typischen 64-Bit-JVM mit komprimierten Class Pointern bestand dieser Header bisher aus einem 64 Bit großen Mark Word und einem 32 Bit großen Class Word, also insgesamt aus 96 Bit bzw. 12 Byte. Gerade bei vielen kleinen Objekten haben diese 12 Byte einen erheblichen Anteil am gesamten Speicherverbrauch.

Project Lilliput arbeitet deshalb schon länger daran, diesen Overhead zu reduzieren. Die mit Java 24 zunächst experimentell eingeführten Compact Object Headers integrieren den Class Pointer in das Mark Word und reduzieren den Header damit auf 64 Bit beziehungsweise acht Byte. Java 25 machte die Technik zu einem regulären Feature, sie musste aber weiterhin explizit aktiviert werden. Der JEP 534 macht Compact Object Headers mit Java 27 nun zum Standard. Wer zum bisherigen Layout zurückkehren will, kann es vorerst noch deaktivieren:

java ... -XX:-UseCompactObjectHeaders

Vier eingesparte Byte erscheinen zunächst wenig. Java-Anwendungen bestehen aber häufig aus sehr vielen kleinen Objekten. Je kleiner ein Objekt ist, desto größer ist der relative Anteil seines Headers. Vom OpenJDK veröffentlichte Messungen zeigen entsprechend deutliche Effekte. In einem SPECjbb2015-Szenario sank der Heap-Bedarf um 22 Prozent und die CPU-Zeit um 8 Prozent. In anderen Messungen sank die Zahl der Garbage Collections um 15 Prozent. Ein paralleler JSON-Parser lief rund 10 Prozent schneller.

Das liegt nicht nur am geringeren Heap-Verbrauch. Kleinere Objekte verbessern auch die Datenlokalität und damit die Nutzung der CPU-Caches. Gleichzeitig muss der Garbage Collector weniger Speicher verarbeiten. Wie stark eine reale Anwendung profitiert, hängt natürlich vom Objektmodell ab. Bei Millionen kleiner Objekte kann der Effekt erheblich sein, bei wenigen sehr großen Arrays dagegen kaum. Für normalen Java-Code ist die Änderung aber transparent. Etwas mehr Aufmerksamkeit verdient die Änderung in nächster Zeit bei JVM-nahen Tools, Agents oder Bibliotheken, die Annahmen über das konkrete Speicherlayout von Objekten treffen. Sie müssen gegebenenfalls angepasst beziehungsweise aktualisiert werden.

G1 wird wirklich überall zum Default

Seit Java 9 gilt G1 als Standard-Garbage-Collector der HotSpot VM. Ganz korrekt war diese Aussage bisher allerdings nicht. Auf besonders kleinen Systemen wählte die JVM stattdessen automatisch den Serial GC. Das betraf Umgebungen mit nur einer verfügbaren CPU oder weniger als 1 792 MByte physischem Speicher. Zum Zeitpunkt der Einführung von G1 als Default bot Serial dort Vorteile bei Durchsatz und Speicherbedarf. Inzwischen wurde G1 jedoch auch für kleine Heaps und eingeschränkte Umgebungen erheblich optimiert. Insbesondere die mit Java 26 vorgenommenen Verbesserungen zur Reduzierung von Synchronisation haben den Abstand beim Durchsatz weiter verringert. Der JEP 523 entfernt deshalb die Sonderregel nun. Wenn kein Garbage Collector explizit angegeben wird, verwendet Java 27 immer G1. Aber der Serial GC verschwindet dadurch nicht. Er lässt sich weiterhin explizit auswählen:

java ... -XX:+UseSerialGC

Für die meisten Anwendungen ändert sich ebenfalls nichts. Wer bereits auf einer Maschine mit mehreren CPUs und ausreichend RAM arbeitet, nutzte ohnehin G1. Interessant ist die Änderung dagegen für kleine Container (Docker, …). Bislang konnte die Änderung des CPU- oder der Memory-Limits dazu führen, dass dieselbe Anwendung plötzlich mit einem anderen Garbage Collector startete. Dieses implizite Verhalten entfällt nun.

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Post-Quantum-Kryptografie für TLS 1.3

Eine der wichtigsten Neuerungen von Java 27 beschäftigt sich mit einer Bedrohung, die heute noch gar nicht praktisch existiert. Heutige TLS-Verbindungen verwenden für den Schlüsselaustausch unter anderem elliptische Kurven. Ein ausreichend leistungsfähiger Quantencomputer könnte die dafür zugrunde liegenden mathematischen Probleme mit dem Shors-Algorithmus sehr schnell knacken. Solche Computer stehen heute zwar noch nicht zur Verfügung; für langlebige vertrauliche Daten ergibt sich trotzdem bereits ein Problem. Ein Angreifer kann verschlüsselten Netzwerkverkehr heute aufzeichnen und ihn möglicherweise Jahre später entschlüsseln. Dieses Szenario wird als „harvest now, decrypt later“ bezeichnet.

Mit JEP 527 unterstützt die TLS-1.3-Implementierung von Java deshalb drei neue, hybride Schlüsselaustauschverfahren:

  • X25519MLKEM768

  • SecP256r1MLKEM768

  • SecP384r1MLKEM1024

Sie kombinieren jeweils einen klassischen ECDHE-Schlüsselaustausch mit dem quantenresistenten ML-KEM. Dieser hybride Ansatz ist wichtig, um verschiedene Verfahren zu kombinieren. Gerade die Post-Quantum-Algorithmen sind noch jünger als etablierte Verfahren wie X25519 und haben noch nicht dieselbe jahrzehntelange Analyse hinter sich. Die Verbindung ist sicher, solange mindestens eines der beiden Verfahren nicht gebrochen werden kann. Damit schützt der klassische Anteil vor möglichen zukünftigen Schwächen von ML-KEM, während ML-KEM den Schutz gegen Quantenangriffe auf ECDHE übernimmt.

Diese wichtige Änderung ist für unsere Anwendungen weitgehend transparent. X25519MLKEM768 steht in Java 27 an erster Stelle der standardmäßig angebotenen TLS-1.3-Gruppen. Unterstützt die Gegenstelle ebenfalls dieses Verfahren, wird es automatisch ausgehandelt. Bestehender JSSE-, HTTPS- oder HttpClient-Code profitiert also allein durch das JDK-Update. Und die Auswahl lässt sich bei Bedarf über jdk.tls.namedGroups oder programmatisch über SSLParameters.setNamedGroups() beeinflussen. Das OpenJDK hat alle dafür notwendigen Bausteine schon zuvor eingeführt: Java 21 brachte das Key Encapsulation Mechanism API, Java 24 ML-KEM. Java 27 integriert diese Bausteine nun erstmals in das alltäglich verwendete TLS.

JFR schwärzt sensible Daten

Java Flight Recorder (JFR) ist ein äußerst nützliches Werkzeug zur Diagnose von Produktionssystemen. Gerade deshalb werden .jfr-Dateien häufig zwischen Teams ausgetauscht oder an Hersteller und Support-Dienstleister weitergegeben. Ein Flight Recording kann allerdings auch Informationen enthalten, die man nicht unbedingt weitergeben möchte. Dazu gehören unter anderem:

  • Kommandozeilenargumente

  • Umgebungsvariablen oder

  • System-Properties

Und genau dort finden sich in realen Anwendungen immer wieder Passwörter, Tokens oder andere Zugangsdaten. Zwar lassen sich JFR-Dateien nachträglich bereinigen. Zu diesem Zeitpunkt könnten diese Geheimnisse aber schon abgegriffen worden sein. JEP 536 verlagert die Schwärzung deshalb direkt in die JVM. Sensible Werte werden bereits beim Erzeugen des Events durch [REDACTED] ersetzt und gelangen so gar nicht erst in die Datei. Die Funktion ist mit Java 27 standardmäßig aktiviert.

Beispielsweise wird ein Recording statt javax.net.ssl.keyStorePassword = secret123 nur noch javax.net.ssl.keyStorePassword = [REDACTED] enthalten. Die Erkennung basiert auf konfigurierbaren Filtern für Namen von System Properties und Umgebungsvariablen sowie für Kommandozeilenargumente. Die Defaultregeln erkennen typische Begriffe wie password, secret, token, credential, api-key oder client-secret. Eigene Regeln können über -XX:FlightRecorderOptions ergänzt oder die Schwärzung auch komplett deaktiviert werden. Aber damit folgt auch dieses Feature dem Prinzip sicherer Defaults: Für den Normalfall muss der Benutzer nichts konfigurieren.

Kleinere API-Erweiterungen

Abseits der JEPs gibt es wieder viele kleinere Erweiterungen der Standardbibliothek. Diese finden sich in den Release Notes [3] oder können im Java Almanac [4] nachvollzogen werden. Die Klasse String wurde um eine Methode int encodedLength(Charset charset) ergänzt. Damit lässt sich bestimmen, wie viele Bytes ein String in einer bestimmten Kodierung benötigt. Bisher musste man dafür typischerweise tatsächlich kodieren und erzeugte dadurch ein unnötiges byte-Array. Passend dazu erhalten auch MemorySegment und SegmentAllocator aus dem FFM API neue Methoden, um Strings mit bekannter Länge effizient zwischen Java und Foreign Memory zu übertragen.

Die Klassen Math und StrictMath wurden um die inversen hyperbolischen Funktionen Math.asinh(x), Math.acosh(x) und Math.atanh(x) erweitert, außerdem kann BigDecimal nun direkt eine n-te Wurzel berechnen:

BigDecimal result = value.rootn(3, MathContext.DECIMAL128);

Damit müssen solche Operationen nicht mehr selbst implementiert beziehungsweise über externe Bibliotheken integriert werden.

Eine kleine, aber im Alltag möglicherweise sichtbare Änderung betrifft die vordefinierten DateTimeFormatter. Sie akzeptieren nun auch die von ISO 8601 erlaubten kurzen UTC-Offsets wie 2026-08-22T20:15+02 statt zwingend 2026-08-22T20:15+02:00 zu verlangen. Anwendungen, die externe ISO-8601-Daten verarbeiten, müssen damit weniger häufig eigene Formatter definieren.

Und es wird auch weiterhin aufgeräumt. Während mit Java 26 endgültig das Applet API weggefallen ist, gibt es in Java 27 einen weiteren Schritt bei der Entfernung von finalize. Seit Java 18 wird mit dem JEP 421 am vollständigen Abschied von Finalization gearbeitet, nachdem dieser Mechanismus bereits in Java 9 deprecated und in Java 11 der Inhalt der finalize-Methoden entfernt wurde. Jetzt wurde mit ThreadPoolExecutor.finalize() eine weitere verbliebene Finalizer-Methode aus der Standardbibliothek gelöscht. Normaler Anwendungscode bemerkt davon nichts. Wer von ThreadPoolExecutor abgeleitet und explizit super.finalize() aufgerufen hat, wird nun mit Java 27 allerdings einen Fehler bekommen. Denn der Aufruf fällt nun auf Object.finalize() zurück, dessen Signatur aber ein Throwable wirft.

Wiedervorgelegte Preview- und Inkubator-Features

Von den neun JEPs in Java 27 sind fünf keine wirklich neuen Features. Vier Preview-Funktionen und ein Inkubator-API wurden erneut vorgelegt. Während es bei Lazy Constants, Structured Concurrency und dem PEM API tatsächlich noch Änderungen gab, sind Primitive Patterns und das Vector API aber praktisch unverändert geblieben.

Lazy Constants – dritte Preview

Lazy Constants wurden in Java 25 zunächst unter dem Namen Stable Values eingeführt und mit Java 26 erheblich vereinfacht und umbenannt. Sie ermöglichen Werte, die erst beim ersten Zugriff berechnet werden sollen, danach aber dauerhaft unveränderlich bleiben. Bisher werden static final-Felder im Rahmen der Klasseninitialisierung berechnet. Das ist effizient – solange die Konstanten auch tatsächlich benötigt werden. In vielen realen Anwendungen existieren jedoch komplexe oder umfangreiche statische Strukturen, die möglicherweise nie oder nur selten verwendet werden. Die teure Initialisierung von Patterns (reguläre Ausdrücke) ist ein Beispiel:


static final Map<String, Pattern> PATTERNS = Map.of(
  "email", Pattern.compile("..."),
  "phone", Pattern.compile("..."),
  "zip", Pattern.compile("...")
);

Beim Laden der Klasse werden alle regulären Ausdrücke kompiliert – unabhängig davon, ob sie im konkreten Anwendungslauf überhaupt benötigt werden. Lazy Constants erlauben es, solche Werte erst beim ersten tatsächlichen Zugriff zu initialisieren. Die JVM übernimmt dabei die korrekte, Thread-sichere Initialisierung, ohne dass Entwickler auf Double-checked Locking oder ähnliche Konstruktionen zurückgreifen müssen. Das reduziert unnötige Objektallokationen und aufwändige Initialisierungsarbeit beim Laden der Klassen und verkürzt insbesondere bei großen frameworkbasierten Anwendungen die Start-up-Zeiten. Gerade in Microservices mit vielen Klassen und Konfigurationsobjekten kann sich das messbar auswirken. Listing 1 zeigt ein einfaches Beispiel für einen Logger.

Listing 1: LazyConstants im Einsatz

private final LazyConstant<Logger> logger = 
  LazyConstant.of(() -> 
    Logger.create(OrderController.class));

void submitOrder(User user, List<Product> products) {
  logger.get().info("order started");
  // ...
  logger.get().info("order submitted");
}

Die Berechnung erfolgt höchstens einmal und threadsicher. Da die JVM anschließend weiß, dass sich der Wert nicht mehr verändern kann, kann sie Zugriffe ähnlich optimieren wie bei normalen Konstanten. Im JEP 531 gab es jetzt zwei Änderungen. Die Methoden isInitialized() und orElse(…) wurden entfernt. Sie erlaubten dem Benutzer, den Initialisierungszustand zu beobachten und das Programmverhalten davon abhängig zu machen. Genau das widerspricht der Abstraktion: Ob eine Lazy Constant bereits berechnet wurde, soll ein Implementierungsdetail bleiben. Neu ist dafür Set.ofLazy(…), ein Beispiel zeigt Listing 2. Ob ein Element tatsächlich zum Set gehört, wird erst beim ersten entsprechenden Zugriff bestimmt und danach gespeichert. Nach List.ofLazy() und Map.ofLazy() gibt es damit für alle drei grundlegenden Collection-Typen eine Lazy-Variante.

Listing 2: Lazy Set mit verzögerter Initialisierung der Werte

enum Option { VERBOSE, DRY_RUN, STRICT }

// Return true when the given Option is enabled
boolean isEnabled(Option option) {
  IO.println(String.format("isEnabled(%s) ...", option.name()));
  // could be read from a configuration file (expensive)
  if (option == Option.DRY_RUN) {
    return false; // DRY_RUN is disabled
  }
  return true;
}

// Lazily initialized Set of Options
final Set<Option> OPTIONS =
  Set.ofLazy(EnumSet.allOf(Option.class), this::isEnabled);

void main() {
  IO.println("started ...");
  if (OPTIONS.contains(Option.DRY_RUN)) {
    IO.println("dry run mode enabled");
  }
  if (OPTIONS.contains(Option.STRICT)) {
    IO.println("strict mode enabled");
  }
  // Actual processing logic
}

Primitive Types in Patterns – fünfte Preview

JEP 532 erweitert Pattern Matching auf primitive Typen. Primitive Werte können dadurch unter anderem als Type Patterns in switch, bei instanceof und innerhalb von Record Patterns auftreten. Hier ein einfaches Beispiel.

String category = switch (flights) {
  case 0                     -> "none";
  case int i when i >= 100   -> "frequent flyer";
  case int i                 -> "regular";
};

Mit Java 21 begann ein fundamentaler Wandel in der Sprache: Pattern Matching trat an die Stelle vieler klassischer instanceof-Checks und komplexer switch-Statements. Mit Records und Sealed Classes lassen sich algebraische Datentypen nutzen und damit invalide Zustände innerhalb der Daten vermeiden. Type Patterns, Pattern Matching in switch, Record Patterns und Unnamed Patterns haben ihr Potential bereits entfaltet. Ideen für weitere Typen (Array Patterns, Deconstructor/Factory Patterns, …) stehen schon in den Startlöchern. Aktuell wird allerdings nur an den im OpenJDK 23 eingeführten Primitive Type Patterns gearbeitet. Sie wurden jetzt als fünfte Preview (JEP 532) ohne Änderungen zum letzten Release erneut herausgebracht. Es soll weiteres Feedback gesammelt werden, bevor sie finalisiert werden.

Beim Pattern Matching geht es darum, bestehende Strukturen mit Mustern abzugleichen, um komplizierte Fallunterscheidungen effizient und wartbar implementieren zu können. Ein Pattern kombiniert dabei ein Prädikat, das auf die Zielstruktur passt, mit einer Menge von Variablen innerhalb dieses Musters. Bei einem Treffer werden diesen Variablen die entsprechenden Inhalte zugewiesen und damit aus der Zielstruktur extrahiert. Die Intention des Pattern Matching ist die Destrukturierung von Datenobjekten, also das Aufspalten in die Bestandteile und das Zuweisen in einzelne Variablen zur weiteren Bearbeitung.

Mit instanceof und switch können wir also überprüfen, ob ein Objekt von einem bestimmten Typ ist, und wenn ja, dieses Objekt einer Variable dieses Typs zuweisen und diese Variable in dem folgenden Programmpfad benutzen. Das funktionierte bisher aber nur mit Objekten und ließ sich nicht mit primitiven Datentypen kombinieren. Einzig im switch ließen sich Variablen der primitiven Typen byte, short, char und int bereits gegen Konstanten matchen. Sie konnten dort sogar mit den neueren Type Patterns kombiniert werden.

JEP 532 und seine Vorgänger verbessern nun die Typprüfung, Performance und Lesbarkeit in Java und machen Pattern Matching konsistenter, indem sie primitive Typen direkt unterstützen. Das reduziert überflüssiges Autoboxing und vereinfacht den Umgang mit primitiven Datentypen in switch-Anweisungen. Wie am Beispiel in Listing 3 zu sehen ist, können Entwickler in Zukunft ganz einfach prüfen, ob ein ganzzahliger Wert in den Wertebereich eines Bytes passt.

Listing 3: Wertebereichsprüfung von primitiven Datentyp

private static String checkByte(int value) {
  if (value instanceof byte b) {
    return "byte b = " + b;
  } else {
    return "kein byte: " + value;
  }
}

System.out.println(checkByte(127)); // b = 127
System.out.println(checkByte(128)); // kein byte: 128

Wichtige Themen beim Pattern Matching sind die Exhaustiveness-Prüfung und die sogenannte Dominanzregel: Letztere stellt sicher, dass kein case-Label unerreichbar wird, weil ein früheres Pattern es bereits abdeckt. Bei Primitive Patterns kommen hier zusätzliche Überlegungen ins Spiel: Da es bei primitiven Typen keine Vererbung gibt, entscheidet der Compiler anhand von exakten und verlustfreien Konvertierungen, welches Pattern Vorrang hat.

Mit der Erweiterung um Primitive Type Patterns wird Pattern Matching konsistent auf die gesamte Sprache ausgedehnt. Entwickler können direkt mit primitiven Werten in Patterns arbeiten, redundanten Code reduzieren und komplexe Auswertungen klarer und kompakter formulieren. Das ist nicht nur syntaktischer Zucker, es trägt zur Ausdrucksstärke und Lesbarkeit bei, reduziert Fehlerquellen und schafft eine einheitlichere Sprache – ein wichtiger Schritt auf dem Weg zu moderner, deklarativer Programmierung in Java.

Structured Concurrency – siebte Preview

Dieses API behandelt eine Gruppe zusammengehöriger nebenläufiger Tasks als eine gemeinsame Arbeitseinheit (Listing 4) und ist damit ein wichtiger Schlüssel zur effizienten Verwendung von Virtual Threads. Mit ihrer Einführung in Java 21 wurde Nebenläufigkeit in Java radikal vereinfacht. Doch Threads allein lösen noch kein strukturelles Problem. Wie orchestriert man vielmehr mehrere parallele Aufgaben so, dass Fehler, Abbrüche und Lebenszyklen sauber kontrolliert bleiben? Genau hier setzt Structured Concurrency an. Im OpenJDK 25 gab es einige größere Änderungen an dem API, die jetzt weiter getestet werden sollen und zu denen man Feedback sammeln möchte.

Listing 4: StructuredTaskScope für zwei Teilaufgaben

try (var scope = StructuredTaskScope.open()) {
  var user   = scope.fork(this::loadUser);
  var orders = scope.fork(this::loadOrders);

  scope.join();

  return new Result(user.get(), orders.get());
}

In der klassischen Nebenläufigkeit wird typischerweise mit Futures oder Executor Services gearbeitet. Die Probleme dabei sind:

  • Die Fehlerbehandlung ist fragmentiert.

  • Wenn eine Aufgabe fehlschlägt, laufen andere möglicherweise weiter.

  • Das Abbrechen von Tasks ist kompliziert.

  • Die Lebensdauer von Tasks ist nicht klar an einen Scope gebunden.

  • Thread-Leaks sind möglich.

Nebenläufigkeit ist so strukturell schwer kontrollierbar. Structured Concurrency behandelt die Nebenläufigkeit wie eine Blockstruktur und verfolgt dabei ein einfaches Prinzip: Nebenläufige Aufgaben gehören in einen gemeinsamen Lebenszyklus – genau wie Methodenaufrufe in einem Block.

Anstatt lose Futures zu erzeugen, werden parallele Tasks in einem Scope gestartet und gemeinsam verwaltet. Der Aufruf von join() wartet strukturiert darauf, dass alle Aufgaben abgeschlossen sind. Falls ein Task fehlschlägt, werden die anderen automatisch abgebrochen. Und nach Verlassen des try-Blocks ist garantiert, dass kein Task weiterlebt. Nebenläufigkeit wird damit lexikalisch strukturiert, ähnlich wie Ressourcen mit try-with-resources.

Seine volle Stärke entfaltet Structured Concurrency in Kombination mit Virtual Threads. Jeder Task kann in einem Virtual Thread laufen und das ohne Thread-Pool-Tuning sowie ohne komplexes Ressourcenmanagement. Das erlaubt ein Programmiermodell, das synchron aussieht, aber massiv parallel skaliert. Und das ohne Callback-Hölle oder den Overhead von reaktiver Programmierung. Im Rahmen moderner Backendarchitekturen passt das hervorragend für:

  • die Aggregation mehrerer Services

  • parallele Datenbank- oder API-Calls

  • Fan-out/Fan-in-Muster

  • Resilienz-Patterns

Gerade in Cloud-Umgebungen mit I/O-lastigen Workloads wird dieses Modell sehr relevant. Und es ist mehr als syntaktischer Komfort. Es verändert das Denkmodell. Vorher haben wir Nebenläufigkeit als Sammlung unabhängiger Futures betrachtet, jetzt ist es strukturierter Teil eines Kontrollflusses.

Zwischen den beiden Extremen – Low-Level-Threads mit komplexer Verwaltung und reaktiven Frameworks mit steiler Lernkurve – ermöglicht Structured Concurrency einen imperativen Stil mit klarer Lebensdauer, robuster Fehlerbehandlung und hoher Skalierbarkeit. In Kombination mit Virtual Threads entsteht damit ein Nebenläufigkeitsmodell, das sowohl gut verständlich/wartbar als auch leistungsfähig ist.

In Java 27 wurde insbesondere die Fehlerbehandlung überarbeitet. StructuredTaskScope und Joiner berücksichtigen nun zusätzlich den Typ der Exception, die join() werfen kann. Dadurch können Joiner genauer ausdrücken, wie Fehler der einzelnen Subtasks in einen Fehler des gesamten Scopes übersetzt werden. Neu ist außerdem awaitAllSuccessfulOrThrow(). Diese Strategie eignet sich gut, wenn die Subtasks unterschiedliche Ergebnistypen besitzen. join() wartet darauf, dass alle erfolgreich sind, anschließend werden die Ergebnisse über die zuvor zurückgegebenen Subtask-Objekte abgefragt.

Daneben wurde das Timeout API aufgeräumt. onTimeout() heißt nun timeout(), und mit CancelledByTimeoutException gibt es einen neuen Exception-Typ für den Abbruch aufgrund eines Zeitlimits. Das bisherige awaitAll() entfällt. Außerdem gibt es nun mit StructuredTaskScope.open(configOperator) die Möglichkeit, einen Scope zu konfigurieren, ohne einen eigenen Joiner anzugeben. Die Grundidee von Structured Concurrency ist längst stabil. Die ungewöhnlich hohe Zahl von inzwischen sieben Preview-Runden entsteht vor allem dadurch, dass im OpenJDK weiterhin an der exakten Form des API gearbeitet wird.

PEM API – dritte Preview

Sicherheit ist in modernen Systemen kein optionales Zusatzfeature, sondern grundlegende Infrastruktur. Zertifikate, Schlüsselpaare, Signaturen und TLS-Konfigurationen gehören zum Alltag – insbesondere in Cloud- und API-zentrierten Architekturen. Ein relevanter Baustein ist das PEM API, das nun zum dritten Mal als Preview dabei ist. PEM-Dateien enthalten beispielsweise Zertifikate und Schlüssel:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi/kRGOL7wCPTN4KJ2ppeSt5UYB6u
cPjjuKDtFTXbguOIFDdZ65O/8HTUqS/sVzRF+dg7H3/tkQ/36KdtuADbwQ==
-----END PUBLIC KEY-----

Obwohl dieses Format allgegenwärtig ist – insbesondere im TLS- und Cloud-Umfeld –, war die direkte Unterstützung im JDK bislang begrenzt. Entwickler mussten häufig auf Drittbibliotheken zurückgreifen oder eigene Parsing-Lösungen implementieren.

Mit JEP 538 und seinen Vorgängern wird jetzt ein natives API bereitgestellt, um PEM-kodierte Objekte direkt zu lesen und zu schreiben. Das API abstrahiert dabei das Header-/Footer-Handling, das Base64-Decoding und die Formatvalidierung. Gerade in modernen Deployment-Szenarien sind PEM-Dateien allgegenwärtig, z. B. in Kubernetes Secrets, Cloud-Zertifikaten, mTLS-Konfigurationen, ACME-/Let’s-Encrypt-Integrationen und API-Gateways. Durch die native Unterstützung werden Boilerplate-Code, Fehlerquellen beim Parsen und Validierung sowie Abhängigkeiten von Drittbibliotheken reduziert. Gerade in sicherheitskritischen Systemen ist es ein Vorteil, wenn kryptografische Operationen möglichst nahe an der Standardplattform bleiben.

In Java 27 wurde insbesondere die Abstraktion für kodierbare Objekte verallgemeinert. Das bisherige DEREncodable wurde durch BinaryEncodable ersetzt. Entsprechend verwenden beispielsweise X509Certificate, KeyPair, PKCS8EncodedKeySpec und EncryptedPrivateKeyInfo nun die allgemeinere Schnittstelle. Auch PEMEncoder akzeptiert BinaryEncodable. Der Grund ist konzeptionell sinnvoll. PEM ist zwar häufig die Base64-Textrepräsentation eines DER-kodierten Objekts, das API soll aber nicht unnötig auf DER als einzig mögliche binäre Repräsentation festgelegt sein.

Auch PEMDecoder wurde weiter verfeinert. Unter anderem wurde aus withFactory(Provider) die präzisere Methode withFactoriesOf(Provider). Und das bisher als Record modellierte PEM wird zu einer normalen Klasse. Hinzu kommt mit CryptoException ein gemeinsamer Exception-Typ für entsprechende Operationen.

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Vector API – zwölfte Inkubator-Runde

Das Vector API hält einen bemerkenswerten Rekord: Java 27 bringt bereits seine zwölfte Inkubatorversion. Es ermöglicht explizite SIMD-Berechnungen (Single Instruction, Multiple Data), die die JVM beispielsweise auf AVX- oder NEON-Instruktionen der jeweiligen CPU abbilden kann (Listing 5). Dabei werden mehrere float-Werte gleichzeitig verarbeitet – je nach CPU z. B. 128 oder 256 Bit breit. SIMD ist entscheidend für numerische Simulationen, Signalverarbeitung, Bildverarbeitung und Machine Learning, dabei insbesondere für Embedding-Berechnungen und Vektorähnlichkeitsvergleiche. Bislang war Java hier oft auf native C-Bibliotheken (über JNI) angewiesen. Das Vector API bringt diese Fähigkeiten direkt in die JVM – portabel und optimierbar durch den Just-in-Time-(JIT-)Compiler.

Listing 5: Gegenüberstellung mit und ohne Vector API

// ohne Vector API
for (int i = 0; i < a.length; i++) {
    c[i] = a[i] + b[i];
}

// mit Vector API
import jdk.incubator.vector.*;

var species = FloatVector.SPECIES_PREFERRED;

for (int i = 0; i < a.length; i += species.length()) {
  var va = FloatVector.fromArray(species, a, i);
  var vb = FloatVector.fromArray(species, b, i);
  var vc = va.add(vb);
  vc.intoArray(c, i);
}

Für Java 27 gibt es allerdings keine wesentlichen API-Änderungen. Die erneute Inkubation ist vor allem eine Folge der Abhängigkeit von Project Valhalla. Denn Vektoren sind typische Kandidaten für Value Classes: Sie sollen sich wie Werte verhalten, keine relevante Objektidentität besitzen und möglichst ohne klassischen Objekt-Overhead verarbeitet werden. OpenJDK möchte deshalb vermeiden, das heutige API zu standardisieren und es nach der Einführung entsprechender Valhalla-Funktionen wieder grundlegend ändern zu müssen. Da mit Java 28 im März 2027 ein erster Stand der Value Classes Einzug ins OpenJDK erhält, könnte auch das Vector API bald in den Preview- und anschließend in den finalen Status wechseln.

Fazit und Ausblick

Java 27 ist kein Release der großen Sprachänderungen. Gerade die wichtigsten Neuerungen funktionieren vielmehr weitgehend unterhalb des Anwendungscodes. Compact Object Headers reduzieren automatisch den Speicherbedarf, G1 wird zu einem verlässlicheren Default über unterschiedliche Deployment-Umgebungen hinweg, TLS kann ohne Codeänderungen Post-Quantum-Verfahren aushandeln und JFR schützt sensible Informationen künftig standardmäßig. Daneben zeigen die Wiedervorlagen ein interessantes Bild: Primitive Patterns und das Vector API verändern sich kaum noch, während insbesondere Structured Concurrency, das PEM API und Lazy Constants weiterhin API-seitig nachgeschärft werden. Alle weiteren kleineren Neuerungen ohne eigenen JEP finden sich in den Release Notes [3]. Änderungen am JDK beziehungsweise an der Java-Klassenbibliothek lassen sich zudem übersichtlich im Java Almanac [4] nachvollziehen.

Damit steht Java 27 vor allem für bessere Defaults, Sicherheit und Konsolidierung. Viele der Verbesserungen sind nicht spektakulär, können bestehende Anwendungen aber bereits allein durch den Wechsel auf die neue JVM effizienter oder sicherer machen. Und nächstes Jahr kommen mit Project Valhalla und JEP 401 die Value Classes als nächste sehr große Änderung am Typsystem von Java. Sie versprechen:

  • Objekte ohne Identität

  • kein Objekt-Header

  • kompakteres Speicherlayout

  • bessere Cache-Lokalität

  • deutlich geringeren GC-Druck

  • explizitere Null-Semantik

Damit verschwimmt die historische Grenze zwischen primitiven Typen und Referenztypen immer weiter. Domänenmodelle könnten künftig aus echten Werttypen bestehen – ohne Performancenachteile durch den Overhead der Objekt-Header. Für viele Anwendungsfälle ist das hochrelevant, z. B. numerische Simulationen, Finanzberechnungen, Embedding- und Vektorverarbeitung, große Datenstrukturen und KI-nahe Workloads. Java wird damit nicht nur syntaktisch, sondern auch speichertechnisch modernisiert.

Parallel dazu verschiebt sich das Sicherheits- und Integritätsmodell der Plattform. Unter dem Leitgedanken „Integrity by Default“ wird die Möglichkeit eingeschränkt, Objektinvarianten über Reflection oder Deserialisierung zu umgehen. Gerade in großen Architekturen, in denen Invarianten ein zentrales Designinstrument sind, stärkt das die Verlässlichkeit des Codes. Und für eine moderne Serialisierung werden Deconstruction Patterns für eine gleichberechtigte Unterstützung von Konstruktion und Dekonstruktion sorgen. Wenn ein Objekt konstruiert werden kann, sollte es auch strukturell zerlegt werden können – als First-class-Sprachkonzept. Konzeptionell könnte das wie in Listing 6 aussehen (Vorsicht, das ist nur ein erster Vorschlag!).

Listing 6: Mögliche Syntax für Dekonstruktoren

public class Point {
  private final int x;
  private final int y;

  public Point(int x, int y) {
    this.x = x;
    this.y = y;
  }

  public pattern Point(int x, int y) {
    x = this.x;
    y = this.y;
  }
}

// im Pattern Matching:
if (obj instanceof Point(var x, var y)) {
  System.out.println(x + "," + y);
}

Konstruktion und Dekonstruktion wären damit symmetrisch und bildeten ein mächtiges Konzept für klarere Geschäftslogik, bessere Pattern-basierte Auswertungen, weniger Boilerplate und saubere Serialisierung. Sogenannte Carrier Classes waren Anfang des Jahres als Grundlage für das neue Serialisierungsmodell in der Diskussion [5]. Statt Konstruktoren zu umgehen oder final-Felder per Reflection zu manipulieren, würde Serialisierung explizit über definierte Deconstruction-Patterns laufen. Das würde die Invarianten respektieren, die Versionierung vereinfachen und Framework-Abhängigkeiten reduzieren, etwa zu Jackson bei der JSON-Serialisierung. Das JDK und die Sprache Java würden damit Lücken schließen, die seit Jahrzehnten von Drittbibliotheken gefüllt waren.

Setzt man all diese Entwicklungen zusammen, ergibt sich ein klarer Trend: Java modernisiert nicht nur einzelne APIs – es überarbeitet sein Fundament: Value Classes verändern das Speicherlayout, Integrity by Default stärkt das Objektmodell, Deconstruction Patterns erweitern das Sprachkonzept und Vector API sowie GC-Optimierungen adressieren Hardwareeffizienz. Start-up-Optimierungen machen Java zudem Cloud-tauglicher. Die Ideen für die nächsten Funktionen gehen den JDK-Entwicklern also nicht aus. Wer sich vorab über weitere mögliche Zukunftsthemen informieren möchte, kann sich schon mal im JEP-Index unter „Draft and submitted JEPs“ [6] umschauen. Die Zukunft für Java-Entwickler wird spannend bleiben.

The post Java 27: Die wichtigsten Neuerungen appeared first on JAX.

]]>
W-JAX Together: Neues Diskussionsformat für IT-Teams https://jax.de/blog/jax-together-diskussionsformat/ Mon, 07 Sep 2026 12:38:07 +0000 https://jax.de/?p=210846 Das umfangreiche Vortragsprogramm der W-JAX bringt viele technologische Themen und Perspektiven auf die Bühne. W-JAX Together gibt den Diskussionen, die daraus entstehen, erstmals einen festen Rahmen im Programm. In moderierten Gesprächsformaten können die Beteiligten das Gehörte aufgreifen, eigene Erfahrungen einbringen und unterschiedliche Einschätzungen miteinander abgleichen.

The post W-JAX Together: Neues Diskussionsformat für IT-Teams appeared first on JAX.

]]>
Du kommst aus einer Session und würdest am liebsten noch weiterreden. Eine These passt nicht zu dem, was ihr im Projekt erlebt. Oder du hast im Vortrag eine Antwort bekommen und bist dabei auf eine neue Frage gestoßen. Doch auf dem Flur wird es voll, und gleich beginnt der nächste Vortrag.

Für solche Momente gibt es auf der W-JAX erstmals das W-JAX Together. Das neue Format schafft einen Ort für Austausch und bringt Teilnehmer:innen, Speaker und Expert:innen in strukturierten Gesprächsformaten zusammen. Hier können offene Fragen weiterdiskutiert und vor allem eigene Erfahrungen eingebracht werden.

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Moderiert wird das W-JAX Together von Wolfgang Pleus, der die JAX seit vielen Jahren als Speaker, Moderator und Kurator mitgestaltet. Seit mehr als 30 Jahren begleitet er IT-Projekte – vom Start-up bis zum Großkonzern – und hat dabei immer wieder erlebt, welchen Einfluss sorgfältig gestaltete Kommunikation auf deren Erfolg hat.

„Der Schlüssel liegt in der Zusammenarbeit der Menschen. Dafür braucht es gute Strukturen, die Kommunikation unterstützen und Gruppen dabei helfen, zu guten Ergebnissen zu kommen.“

Diese Erfahrung bringt Pleus mit W-JAX Together auf die Konferenz.

Weiterdenken im gemeinsamen Gespräch

Gerade beim Thema KI erlebt Pleus in seiner Arbeit mit Teams großen Gesprächsbedarf. Die Veränderungen reichen weit über technische Fragen hinaus, viele Entwicklungen und ihre Folgen lassen sich kaum abschätzen. Der Austausch von Erfahrungen und Einschätzungen hilft, sich in dieser offenen Situation zu orientieren.

„Dafür einen diskursiven Rahmen zu schaffen, in dem wir gemeinsam überlegen können: Was wollen wir tun – und was müssen wir tun? Welche Verantwortung wollen wir übernehmen? Was ist vernünftig – und was nicht? Das halte ich gerade in der aktuellen Situation für sehr wichtig.“

Für Pleus ist eine Konferenz deshalb der passende Ort, um den Gesprächsraum über einzelne Teams und Unternehmen hinaus zu öffnen:

„Ich finde eine Konferenz dafür besonders spannend, weil die Gruppe diverser ist. Es gibt mehr Meinungen, mehr Haltungen und mehr Erfahrungen im Raum. Genau diese unterschiedlichen Perspektiven zusammenzubringen, ist die Idee des W-JAX Together.“

Gute Diskussion braucht Struktur

Damit aus dieser Vielfalt ein produktiver Austausch entsteht, setzt W-JAX Together auf moderierte Methoden und bewährte Spielregeln. Zum Einsatz kommen unter anderem Lean Coffee, UX Fishbowl und verschiedene Liberating Structures.

Die Methoden regeln zum Beispiel, wie Themen ausgewählt werden, wer wann zu Wort kommt und wie die Gruppe ihre Ergebnisse zusammenträgt. Denn Pleus kennt aus seiner Projektarbeit auch die andere Seite:

„Viele klassische Formate sind nur wenig partizipativ. Häufig prägen besonders meinungsstarke Menschen das Ergebnis.“

Bei Lean Coffee bringt die Gruppe selbst Themen ein und priorisiert sie. 1-2-4-All lässt Teilnehmer zunächst allein über eine Frage nachdenken und führt ihre Gedanken anschließend in immer größeren Gruppen zusammen. In einer UX Fishbowl kann man zuhören und sich im Verlauf selbst in die Diskussion einschalten.

Welche Methode zum Einsatz kommt, hängt vom Thema ab. Allen Formaten geht es darum, möglichst viele Erfahrungen und Perspektiven in die Diskussion zu holen.

„Die Haltung dahinter ist, dass nicht ein Einzelner alles weiß. Die Gruppe verfügt gemeinsam über mehr Wissen und mehr Erfahrung als eine einzelne Person, die alles vorgibt.“

Diskutieren – und dabei lernen, wie Diskussion gelingt

Damit hat W-JAX Together noch einen weiteren praktischen Mehrwert: Die Formate lassen sich mitnehmen. Wer ein Lean Coffee, eine Fishbowl oder eine andere Liberating Structure erlebt, bekommt Handwerkszeug für den eigenen Arbeitsalltag. Eine Methode, die auf der W-JAX funktioniert, lässt sich später mit dem eigenen Team ausprobieren.

Für Pleus gehört dazu auch die Erfahrung, selbst wirksam zu werden:

„Man ist nicht darauf angewiesen, was andere tun, sondern kann sich selbst einbringen. Man kommt ins Handeln und erlebt, dass der eigene Beitrag etwas bewirkt.“

Vielleicht gehst du also mit einer Frage ins W-JAX Together und mit einer neuen Perspektive wieder hinaus. Manchmal beginnt die eigentliche Diskussion eben erst nach dem Vortrag.

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Wie du teilnehmen kannst

Die W-JAX Together-Formate finden parallel zu den zahlreichen Vorträgen der Hauptkonferenz statt. Mitmachen kann jede oder jeder, der/die ein Thema weiterdiskutieren, eine eigene Frage einbringen oder einfach zuhören und sich einklinken möchte. Eigene Beiträge, Erfahrungen und Ideen sind ausdrücklich erwünscht. Weitere Informationen und das laufend aktualisierte Programm findest du unter jax.de/together.

The post W-JAX Together: Neues Diskussionsformat für IT-Teams appeared first on JAX.

]]>
Spring AI 2.0: Prompting, Chat Memory und Structured Output https://jax.de/blog/spring-ai-2-0-prompting-chat-memory-structured-output/ Mon, 31 Aug 2026 14:41:19 +0000 https://jax.de/?p=210816 Eine Spring-AI-Anwendung muss Prompts zuverlässig aufbauen, frühere Nachrichten einer Konversation berücksichtigen und Modellantworten in weiterverarbeitbare Daten überführen. Dieser Artikel zeigt, wie Spring AI 2.0 mit ChatClient, Chat Memory und Structured Output die zentralen Bausteine für solche Anwendungen bereitstellt.

The post Spring AI 2.0: Prompting, Chat Memory und Structured Output appeared first on JAX.

]]>

Hinweis: Dieser Podcast und dieses Video wurden mithilfe von KI erstellt. Dabei wurden die Originalinhalte und technischen Erkenntnisse des Autors des Blogbeitrags adaptiert.

 

Eine Spring-Boot-Anwendung soll eingehende Supporttickets automatisch beantworten, E-Mails zusammenfassen oder Informationen aus internen Dokumenten bereitstellen. Für solche Aufgaben eignen sich Large Language Models besonders gut, da sie natürliche Sprache verstehen und generieren können. Viele Funktionen, die früher komplexe Regeln oder spezialisierte Machine-Learning-Verfahren erforderten, lassen sich heute durch einen passenden Prompt beschreiben.

Die direkte Integration eines LLM ist jedoch anspruchsvoller, als es zunächst vielleicht erscheinen mag. OpenAI, Anthropic, Mistral und lokal betriebene Modelle über Ollama stellen jeweils eigene APIs, SDKs und Konfigurationsparameter bereit. Wer sich direkt an einen Anbieter bindet, muss seine Anwendung bei einem Modell- oder Anbieterwechsel anpassen. Gleichzeitig unterscheiden sich LLMs grundlegend von klassischer Anwendungslogik: Sie antworten nicht deterministisch, verursachen tokenbasierte Kosten und können Informationen erzeugen, die nicht den bereitgestellten Fakten entsprechen.

Spring AI abstrahiert die unterschiedlichen APIs und Konfigurationsparameter der Modelle. Die Interfaces ChatClient und ChatModel bieten ein einheitliches API für verschiedene Anbieter und erleichtern den Wechsel zwischen ihnen erheblich.

Darüber hinaus stellt Spring AI die Infrastruktur und eine Reihe von Features bereit, um die genannten Schwierigkeiten zu behandeln und produktive AI-Anwendungen zu erstellen. Dazu gehören Advisors als technischer Erweiterungsmechanismus sowie darauf aufbauende Features wie Chat Memory, Structured Output, Retrieval-augmented Generation (RAG), Tool Calling und das Model Context Protocol (MCP).

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Das erste LLM mit Spring AI aufrufen

Bevor das erste LLM aufgerufen werden kann, benötigt das Projekt lediglich zwei Dinge: eine Abhängigkeit zur gewünschten Modellintegration und einige wenige Konfigurationsparameter.

Seit Spring AI 2.0 wird ein Chatmodell ausschließlich über einen modellspezifischen Spring-Boot-Starter integriert. Soll beispielsweise ein GPT-Modell von OpenAI verwendet werden, genügt die Abhängigkeit zu spring-ai-starter-model-openai. Für lokal betriebene Modelle über Ollama wird stattdessen spring-ai-starter-model-ollama eingebunden. Weitere Spring-AI-Abhängigkeiten sind für den Einstieg nicht erforderlich.

Anschließend muss das verwendete Modell konfiguriert werden. Bei extern betriebenen Modellen geschieht das in erster Linie über einen API Key, der bei OpenAI beispielsweise über den Konfigurationsparameter spring.ai.openai.api-key bereitgestellt wird. Der API Key sollte dabei niemals direkt in der Anwendungskonfiguration hinterlegt, sondern als Umgebungsvariable referenziert werden. Darüber hinaus können weitere Parameter angepasst werden, etwa das zu verwendende Modell über spring.ai.openai.chat.options.model oder eine alternative API-Adresse über spring.ai.openai.base-url. Für einfache Anwendungen reichen die Standardwerte jedoch aus.

Lokale Modelle benötigen keinen API Key. Stattdessen muss das gewünschte Modell ausgewählt werden. Bei Ollama wird das über den Konfigurationsparameter spring.ai.ollama.chat.model realisiert, dessen Wert beispielsweise qwen3:8b oder gemma3 sein kann. Spring AI verbindet sich anschließend automatisch mit der lokal laufenden Ollama-Instanz.

Sind Abhängigkeit und Konfiguration vorhanden, genügen für den ersten Aufruf eine ChatClient-Instanz und drei Methodenaufrufe. Der Service in Listing 1 implementiert einen einfachen Supportticketassistenten.

Listing 1

@Service
public class TicketAssistantService {

  private final ChatClient chatClient;

  public TicketAssistantService(ChatClient.Builder chatClientBuilder) {
    this.chatClient = chatClientBuilder.build();
  }

  public Flux<String> answer(String ticketText) {
    return chatClient.prompt()
      .user(ticketText)
      .stream()
      .content();
  }
}

Wenn nur ein anbieterspezifischer Spring-Boot-Starter im Projekt verfügbar ist, kann Spring die ChatClient.Builder-Instanz per Constructor Injection bereitstellen. Mit ihr kann dann eine ChatClient-Instanz konfiguriert und erzeugt werden. In diesem einfachen Beispiel wird auf eine weitere programmatische Konfiguration verzichtet.

Die prompt()-Methode der ChatClient-Instanz kann mit einem String– oder Prompt-Objekt aufgerufen werden, um einen Prompt zu erzeugen, oder, wie in diesem Beispiel, ohne Parameter. Dann wird der Prompt über ein Fluent API erzeugt. Mit Hilfe der user()-Methode wird dabei der User-Prompt definiert. Durch Aufruf der system()-Methode kann ein System-Prompt gesetzt werden. Die unterschiedlichen Aufgaben beider Prompts betrachten wir im nächsten Abschnitt genauer.

Die stream()-Methode führt mit dem definierten Prompt einen reaktiven Aufruf des LLM aus. Die generierte Antwort wird dann als Flux<String> zurückgegeben, sodass die Oberfläche bereits Teile der Antwort anzeigen kann, während das Modell noch generiert. Da die vollständige Generierung einer längeren Antwort mehrere Sekunden dauern kann, bietet der reaktive Aufruf häufig ein besseres Nutzererlebnis. Für Anwendungsfälle ohne Streaming-Anforderung steht mit der call()-Methode eine blockierende Alternative zur Verfügung.

Der gezeigte Code bietet bereits eine vollständige Integration eines LLM. Die Supportanfrage wird an das Modell übergeben und es generiert eine Antwort. Die Qualität der Antworten wird allerdings noch stark schwanken. Das LLM kennt seine Rolle nicht und weiß auch nicht, welche Art von Antwort erwartet wird. In der Praxis besteht der nächste Schritt deshalb fast immer darin, den Prompt gezielt zu verbessern. Bevor wir das Thema jedoch vertiefen, möchte ich kurz die Struktur einer Spring-AI-Anwendung vorstellen.

Architektur einer Spring-AI-Anwendung

Hinter dem einfachen API-Aufruf aus dem vorherigen Abschnitt verbirgt sich eine mehrschichtige Architektur. Das ChatClient-Interface ist für die meisten Anwendungen das zentrale API zur Arbeit mit einem LLM. Es bietet ein Fluent API, mit dem sich Prompts bauen und ausführen sowie verschiedene andere Features aktivieren und konfigurieren lassen. Das Interface setzt auf dem ChatModel– und dem StreamingChatModel-Interface auf, über die das LLM mittels eines blockierenden oder reaktiven API aufgerufen werden kann. Für beide Interfaces werden Implementierungen durch anbieterspezifische Spring-Boot-Starter bereitgestellt und konfiguriert. Diese folgen dem Namenspattern spring-ai-starter-model-<Anbieter>, z. B. spring-ai-starter-model-openai für die Integration der von OpenAI angebotenen GPT-Modelle.

Für die Integration extern betriebener Modelle wird darüber hinaus ein API Key benötigt. Er kann über anbieterspezifische Konfigurationsparameter, z. B. spring.ai.openai.api-key konfiguriert werden. Der API Key sollte jedoch nie direkt in der Anwendungskonfiguration hinterlegt, sondern beispielsweise als Umgebungsvariable referenziert werden. Das verhindert, dass er über die Konfigurationsdatei versehentlich in einem Repository veröffentlicht wird.

Um den ChatModel-Aufruf herum legt Spring AI eine Kette aus Advisors, vergleichbar mit Servlet-Filtern. Ein Advisor kann den Prompt vor dem Aufruf verändern und die Antwort danach prüfen oder anpassen. Mehrere Advisors lassen sich zu einer Kette kombinieren und in einer festgelegten Reihenfolge ausführen. Dadurch lassen sich Cross-cutting Concerns wie Logging, Guardrails oder Chat Memory sauber von der Anwendungslogik trennen, statt sie in jedem Service erneut zu implementieren. Dieses Prinzip bildet die Grundlage vieler Spring-AI-Features. Ob Chat Memory, Structured Output oder später RAG: Sie alle erweitern den Prompt vor dem Modellaufruf oder verarbeiten die Antwort danach.

Prompt Engineering mit Spring AI

Das Erstellen und schrittweise Verbessern eines Prompts wird als Prompt Engineering bezeichnet. Ziel ist es, einen Prompt zu entwickeln, auf dessen Basis das LLM zuverlässig Antworten erzeugt, die den fachlichen Anforderungen entsprechen. Dafür existiert eine Vielzahl etablierter Techniken, deren vollständige Betrachtung den Rahmen dieses Artikels sprengen würde. Für den Einstieg in Spring AI reichen jedoch einige wenige Konzepte aus, die in nahezu jeder AI-Anwendung zum Einsatz kommen.

In der Praxis entstehen gute Prompts außerdem selten beim ersten Versuch. Sie werden anhand realer Anfragen kontinuierlich verbessert und an den jeweiligen Anwendungsfall angepasst. Dabei geht es weniger darum, die perfekte Formulierung zu finden, sondern dem Modell alle Informationen bereitzustellen, die es für eine gute Antwort benötigt.

Eine wichtige Grundlage bildet die Trennung zwischen System-Prompt und User-Prompt. Der System-Prompt beschreibt die Rolle des Modells sowie allgemeine Vorgaben, beispielsweise den gewünschten Schreibstil, die Zielgruppe oder Einschränkungen bei der Beantwortung. Der User-Prompt enthält dagegen die konkrete Anfrage. Im Beispiel des Supportticketassistenten beschreibt der System-Prompt einen erfahrenen Supportmitarbeiter, während der User-Prompt den eigentlichen Tickettext enthält.

Ebenso wichtig ist die Bereitstellung zusätzlicher Informationen. Ein LLM kann ausschließlich auf Basis der Daten antworten, die ihm im Prompt zur Verfügung stehen. Daher werden häufig weitere Informationen wie Produktnamen, Dokumente oder frühere Nachrichten einer Konversation ergänzt. Je vollständiger der bereitgestellte Kontext ist, desto fundierter und anwendungsspezifischer kann das Modell antworten. Im Beispiel in Listing 2 wird deshalb neben dem eigentlichen Ticket auch der Name des betroffenen Produkts übergeben.

Eine weitere bewährte Technik ist das Few-Shot Prompting. Dabei enthält der Prompt zusätzlich ein oder mehrere Beispiele für gewünschte oder unerwünschte Antworten. Das Modell erkennt dadurch leichter, welches Ergebnis erwartet wird, und kann dieses Muster auch auf neue Eingaben übertragen.

Schließlich sollte ein Prompt möglichst präzise beschreiben, wie das Ergebnis aussehen soll. Neben der eigentlichen Aufgabe können beispielsweise Tonalität, Umfang oder die gewünschte Struktur der Antwort vorgegeben werden. Ein Prompt, der lediglich „Beantworte das Ticket“ verlangt, liefert deutlich schwankendere Ergebnisse als einer, der zusätzlich den gewünschten Stil und den Aufbau der Antwort definiert.

Der TicketAssistantService in Listing 2 setzt einige dieser Techniken mit Spring AI um.

Listing 2

@Service
public class TicketAssistantService {

  private static final String SYSTEM\_PROMPT = """
    You are an experienced support engineer for our SaaS product.
    Answer precisely, suggest a concrete next step and avoid speculation.
    """;

  private final ChatClient chatClient;
  private final PromptTemplate userPromptTemplate =
    new PromptTemplate("Product: {product}\\nTicket:\\n{ticketText}");

  public TicketAssistantService(ChatClient.Builder chatClientBuilder) {
    this.chatClient = chatClientBuilder
      .defaultSystem(SYSTEM\_PROMPT)
      .build();
  }

  public Flux<String> answer(String product, String ticketText) {
    var userMessage = userPromptTemplate.create(
      Map.of("product", product, "ticketText", ticketText));
    return chatClient.prompt(userMessage).stream().content();
  }
}

Mit Spring AI kann der System-Prompt über die defaultSystem()-Methode der ChatClient.Builder-Instanz als Standard für alle daraus erzeugten ChatClient-Instanzen definiert werden. Soll die Rolle des Modells zwischen verschiedenen Aufrufen variieren, kann stattdessen die system()-Methode des ChatClient verwendet werden.

Für strukturierte User-Prompts bietet Spring AI mit der PromptTemplate-Klasse eine einfache Möglichkeit, Platzhalter in einem Template zu ersetzen. Im Beispiel werden dadurch der Produktname und der Tickettext zu einem einheitlich aufgebauten Prompt zusammengesetzt. Das erleichtert die Wiederverwendung von Prompts und sorgt dafür, dass Informationen stets in derselben Struktur an das Modell übergeben werden.

System-Prompt und der Text des PromptTemplate können außerdem als Resource in externen Dateien abgelegt werden. Dadurch lassen sich Prompts unabhängig vom Anwendungscode versionieren und anpassen.

Fast alle Aufrufe eines LLM basieren auf einem Prompt. Daher bildet er auch für viele weitere Spring-AI-Features eine zentrale Grundlage. Chat Memory und Retrieval-augmented Generation folgen beispielsweise demselben Grundprinzip: Sie erweitern den Prompt automatisch um zusätzlichen Kontext, bevor er an das Modell übergeben wird.

NEUES AUS DER JAVA-BACKEND-WELT

Java-Backend-Track entdecken

 

Konversationskontext mit Chat Memory verwalten

Ein LLM ist zustandslos und merkt sich daher keine vorherigen Nachrichten. Jeder Prompt wird unabhängig verarbeitet, ohne Wissen über frühere Anfragen oder Antworten. Beschreibt ein Kunde in einem Supportticket zunächst einen Fehler und stellt anschließend Rückfragen zur Antwort, kennt das Modell diese schon nicht mehr.

Spring AI löst dieses Problem über das Chat-Memory-Feature. Dabei speichert die Anwendung die aktuelle Konversation und stellt sie dem LLM bei jedem Aufruf als Teil des Prompts zur Verfügung. Somit erscheint es dem Nutzer, als ob das LLM sich an die vorherigen Nachrichten erinnern würde. Das ist jedoch nicht der Fall. Das LLM kennt ausschließlich die vorhergehenden Nachrichten, die die Anwendung zum Prompt hinzugefügt hat.

Chat Memory enthält dabei in der Regel nicht alle Nachrichten einer Konversation. Das wird als Chat History bezeichnet und würde zu einem hohen Tokenverbrauch und damit zu hohen Kosten führen. Im Chat Memory wird daher nur ein begrenzter, relevanter Ausschnitt der Konversation gespeichert und dem LLM bereitgestellt. Im einfachsten Fall wird das Chat Memory in Spring AI durch den MessageChatMemoryAdvisor zusammen mit einer ChatMemory-Implementierung bereitgestellt (Listing 3).

Listing 3

@Service
public class TicketChatService {

  private final ChatClient chatClient;

  public TicketChatService(ChatClient.Builder chatClientBuilder, ChatMemory chatMemory) {
    this.chatClient = chatClientBuilder
      .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
      .build();
  }

  public Flux<String> chat(String ticketId, String message, String conversationId) {
    return chatClient.prompt(message)
      .advisors(spec -> spec.param(ChatMemory.CONVERSATION\_ID, conversationId))
      .stream()
      .content();
  }
}

Dazu muss eine MessageChatMemoryAdvisor-Instanz an die defaultAdvisors()-Methode auf der ChatClient.Builder-Instanz oder an die advisor-Methode der ChatClient-Instanz übergeben werden. Der Advisor wird dann vor und nach dem Aufruf des LLM ausgeführt. Vor dem Aufruf übergibt er den auszuführenden Prompt an die ChatMemory-Instanz, fragt von dieser die vorherigen Nachrichten ab und fügt sie dem Prompt hinzu. Nachdem Spring AI die generierte Antwort vom LLM erhalten hat, übergibt der Advisor die Antwort ebenfalls an die ChatMemory-Instanz.

In der Standardkonfiguration verwendet Spring AI ein MessageWindowChatMemory-Objekt mit einem InMemoryChatMemoryRepository als ChatMemory-Implementierung. Es speichert ein Fenster der letzten 20 Nachrichten einer Konversation in einer HashMap. Das Chat Memory geht somit beim Neustart der Anwendung verloren und wird nicht über mehrere Anwendungsinstanzen synchronisiert. Spring AI bietet jedoch auch verschiedene Repository-Implementierungen, die Nachrichten in einer Datenbank oder einem anderen persistenten Speicher ablegen.

Für sehr lange Konversationen, bei denen auch ältere Nachrichten relevant bleiben können, bietet Spring AI darüber hinaus mit dem VectorStoreChatMemoryAdvisor eine vektorbasierte Implementierung des Chat-Memory-Features. Statt eines festen Fensters aus den letzten Nachrichten sucht dieser Advisor per Ähnlichkeitssuche gezielt nach relevanten früheren Nachrichten derselben Konversation. Für die meisten Supportszenarien reicht jedoch das Window-basierte Chat Memory aus dem vorherigen Beispiel.

Beide Varianten der Chat-Memory-Implementierung benötigen eine Conversation-ID. Über sie werden zusammengehörige Nachrichten identifiziert und sie wird der AdvisorSpec bei jedem Aufruf als Parameter übergeben. Da meistens nur der Client eine neue Nachricht einer bestehenden Konversation zuordnen kann, gibt er Teile der Conversation-ID vor. Gleichzeitig muss verhindert werden, dass ein Angreifer diese ID vollständig erraten kann und somit Zugriff auf die Inhalte fremder Konversationen erhält. Daher sollte die Conversation-ID immer mit einer Information kombiniert werden, die vom Client nicht beeinflusst werden kann. Typische Beispiele sind die ID des angemeldeten Nutzers oder eine Session-ID.

Enterprise AI & Agentic Systems

Enterprise AI-Track entdecken

 

LLM-Antworten als strukturierte Java-Objekte verarbeiten

In den bisherigen Beispielen hat das LLM ausschließlich Freitext erzeugt. Das genügt für die Implementierung eines Chatbots, aber nicht, wenn die Antwort durch die Anwendung weiterverarbeitet werden soll. Im Beispiel dieses Artikels kann das LLM etwa eingesetzt werden, um ein Ticket automatisch zu priorisieren. In diesem Fall ist es sinnvoll, das LLM einen definierten Typ anstatt eines Freitexts erzeugen zu lassen. Dieses Verfahren wird als Structured Output bezeichnet.

Spring AI unterstützt Structured Output über ein modellunabhängiges API. Dazu wird das gewünschte Ausgabeformat als Java Record definiert und anschließend an die entity()-Methode übergeben (Listing 4).

Listing 4

public enum Priority { LOW, MEDIUM, HIGH, CRITICAL }

public record TicketClassification(
  String category,
  Priority priority,
  String summary
) {}

@Service
public class TicketClassificationService {

  private final ChatClient chatClient;

  public TicketClassificationService(ChatClient.Builder chatClientBuilder, ChatMemory chatMemory) {
    this.chatClient = chatClientBuilder.build();
  }

  public TicketClassification classify(String ticketText) {
    return classification = chatClient.prompt(ticketText)
      .call()
      .entity(TicketClassification.class);
  }
}

Im Anwendungscode sieht Structured Output damit kaum anders aus als die Erzeugung einer Textantwort. Statt eines Strings liefert Spring AI direkt eine Instanz des gewünschten Typs. Das vereinfacht die Weiterverarbeitung erheblich und vermeidet zusätzlichen Code zum Parsen und Validieren der Antwort.

Intern erzeugt Spring AI mit Hilfe der Jackson-Bibliothek zunächst ein JSON-Schema aus dem übergebenen Typ. Das Schema beschreibt die erwartete Struktur der Antwort und dient dem LLM als Vorgabe. Nach der Generierung wird die Antwort wiederum mit Jackson in das gewünschte Java-Objekt umgewandelt.

Die konkrete Umsetzung hängt vom verwendeten Modellanbieter ab. Unterstützt er ein natives API für Structured Output, nutzt Spring AI es in vielen Fällen automatisch. Andernfalls erweitert Spring AI den Prompt um das erzeugte JSON-Schema sowie zusätzliche Anweisungen zur Ausgabe. Moderne Modelle befolgen diese Vorgaben in der Regel sehr zuverlässig.

Da LLMs dennoch nicht deterministisch arbeiten, kann die Antwort im Einzelfall vom erwarteten Schema abweichen. Der StructuredOutputValidationAdvisor erhöht deshalb die Zuverlässigkeit der Verarbeitung. Er validiert die erzeugte Antwort gegen das erwartete Schema und wiederholt den Modellaufruf mit einer erweiterten Fehlerbeschreibung, falls die Validierung fehlschlägt. Standardmäßig werden bis zu drei Versuche unternommen. Erst wenn auch diese fehlschlagen, gibt Spring AI die zuletzt erzeugte Antwort zurück, und die Anwendung muss die daraus resultierende Exception behandeln.

Bei Anwendungen, die strukturierte Antworten automatisiert weiterverarbeiten, ist diese zusätzliche Absicherung häufig sinnvoll. Im Beispiel der Ticketklassifikation verhindert sie, dass ein ungültiges Ergebnis die anschließende Priorisierung oder das Routing eines Tickets blockiert.

Manche Modelle, darunter aktuelle von Anthropic und OpenAI, unterstützen Structured Output zusätzlich über ein spezialisiertes API. Es ermöglicht eine noch zuverlässigere Erzeugung strukturierter Antworten. Spring AI kann diese APIs für unterstützte Modellanbieter automatisch verwenden. Dazu muss lediglich die Konstante AdvisorParams.ENABLE\_NATIVE\_STRUCTURED\_OUTPUT als Advisor aktiviert werden (Listing 5).

Listing 5

@Service
public class TicketClassificationService {

  private final ChatClient chatClient;

  public TicketClassificationService(ChatClient.Builder chatClientBuilder, ChatMemory chatMemory) {
    this.chatClient = chatClientBuilder
      .defaultAdvisors(AdvisorParams.ENABLE\_NATIVE\_STRUCTURED\_OUTPUT)
      .build();
  }

  public TicketClassification classify(String ticketText) {
    return classification = chatClient.prompt(ticketText)
      .call()
      .entity(TicketClassification.class);
  }
}

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Fazit

Die in diesem Artikel gezeigten Features zum Prompting sowie zur Verwendung von Chat Memory und Structured Output bilden die Grundlage für viele Spring-AI-Anwendungen. Alle drei Bausteine folgen derselben Grundmechanik: Der ChatClient führt einen Prompt aus, während Advisors den Prompt vor seiner Ausführung erweitern und das generierte Ergebnis anschließend verarbeiten.

Für komplexere Anwendungen, die beispielsweise auf eigene Dokumente, externe Daten oder APIs zugreifen müssen, bietet Spring AI weitere Konzepte wie Retrieval-augmented Generation, Tool Calling und das Model Context Protocol. Das grundlegende Zusammenspiel von ChatClient, Prompt und Advisors bleibt dabei jedoch erhalten.

The post Spring AI 2.0: Prompting, Chat Memory und Structured Output appeared first on JAX.

]]>
W-JAX 2026: So überzeugst du deinen Chef https://jax.de/blog/wjax-2026-business-case-teilnahme/ Mon, 31 Aug 2026 12:07:19 +0000 https://jax.de/?p=210804 Du möchtest vom 2. bis 6. November 2026 zur W-JAX nach München, brauchst aber noch die Freigabe deiner Führungskraft? Dann hilft es, die Teilnahme nicht nur als Weiterbildung zu begründen, sondern als Investition in konkrete technische und wirtschaftliche Herausforderungen deines Teams.

The post W-JAX 2026: So überzeugst du deinen Chef appeared first on JAX.

]]>
Genau dafür haben wir die wichtigsten Argumente zusammengestellt. Sie zeigen, wie du Wissen aus dem Programm auf AI-gestützte Entwicklung, Architekturentscheidungen, Security, Compliance und die Modernisierung bestehender Systeme übertragen kannst.

Warum ist 2026 ein wichtiger Zeitpunkt für AI-gestützte Softwareentwicklung?

AI Coding ist längst mehr als Autocomplete im Editor. Der nächste Schritt sind Agenten, die Aufgaben planen, Code erzeugen, Tests ausführen, Tools ansprechen und Teile eines Entwicklungsprozesses eigenständig bearbeiten. Damit verschiebt sich der Engpass: Nicht die reine Code-Erzeugung entscheidet über den Nutzen, sondern die Frage, wie gut Spezifikationen, Architektur, Tests und Qualitätskontrollen vorbereitet sind.

Genau hier setzt das Programm der W-JAX an. Der Enterprise AI Day und zahlreiche Sessions beschäftigen sich damit, wie aus individuellen AI-Experimenten belastbare Engineering-Prozesse für Teams werden.

AI Coding war gestern. Jetzt kommen die Agenten!

Die Keynote bringt die Entwicklung auf den Punkt: Wenn Agenten immer mehr Implementierungsarbeit übernehmen, braucht es klare Regeln dafür, was sie tun dürfen, wie Ergebnisse geprüft werden und an welchen Stellen Menschen bewusst die Kontrolle behalten.

Mit KI und Agenten entwickeln: Workshop für Moderne Softwareentwicklung mit OpenCode, MCP und autonomen Workflows

Der Workshop führt durch einen vollständigen AI-gestützten Entwicklungsablauf, von Spezifikation und Planung über Implementierung und Tests bis zur Dokumentation. Dabei geht es unter anderem um LLMs, MCP, Skills und autonome Workflows. Für Teams, die solche Ansätze bereits evaluieren oder produktiv einsetzen wollen, kann das die eigene Lernkurve deutlich verkürzen.

Harness Engineering: Der Druckreaktor des Agentic Engineering

Wenn Agenten schneller Output produzieren, werden automatische Leitplanken wichtiger. Harness Engineering verbindet Agenten mit technischen und fachlichen Regeln und schafft damit kontrollierbare Feedbackschleifen statt reiner “Vibe Checks”.

Argument für deine Führungskraft: Die eigentlichen Kosten von AI-gestützter Entwicklung entstehen nicht durch das Tool-Abo, sondern durch Trial-and-Error im Projekt. Die W-JAX bündelt Erfahrungen, Patterns und Gegenpositionen, die dein Team sonst selbst erarbeiten müsste.

Wie kannst du Security- und Compliance-Risiken frühzeitig adressieren?

Mit agentischen Systemen entstehen neue technische Risiken. Agenten greifen auf Daten, Tools und APIs zu, externe Modelle werden in bestehende Prozesse eingebunden und Entscheidungen müssen nachvollziehbar bleiben. Gleichzeitig werden Anforderungen aus AI Act und Cyber Resilience Act für Unternehmen konkreter.

Mehrere Sessions der W-JAX übersetzen diese Themen in technische Maßnahmen, die sich in Architektur, Pipelines und Entwicklungsprozesse integrieren lassen.

Building a Secure Java Software Supply Chain in the Era of the Cyber Resilience Act

Die Session zeigt, wie sich Risiken in der Software Supply Chain systematischer beherrschen lassen. Dazu gehören automatisierte CVE-Erkennung und -Behebung, SBOMs, Remediation-Prozesse und Redeployment. Damit bekommt dein Team konkrete Ansatzpunkte, um Anforderungen des Cyber Resilience Act technisch vorzubereiten.

Vier grüne Häkchen, trotzdem gehackt: Threat Modeling für KI-Agenten

RAG-Poisoning, indirekte Prompt Injection und missbrauchte MCP-Tool-Chains lassen sich nicht mit klassischen Checklisten allein abfangen. Die Session betrachtet Trust Boundaries und Architekturkontrollen speziell für agentische Systeme.

Architecting for the EU AI Act: Compliance as a Quality Attribute

Auditierbarkeit, Human Oversight und Daten-Governance werden teuer, wenn sie erst nachträglich in ein System eingebaut werden. Die Session zeigt Architekturansätze für Logging, Isolation, Modellversionierung und gezielte menschliche Eingriffe.

Web-Security up-to-date: Die OWASP Top Ten 2025

Auch die klassischen Webrisiken verschwinden durch AI nicht. Aktuelle OWASP-Themen bleiben deshalb ein wichtiger Teil der technischen Risikovorsorge, gerade wenn neue Agenten und AI-Komponenten an bestehende Anwendungen angeschlossen werden.

Argument für deine Führungskraft: Security und Compliance nachzurüsten ist meist teurer, als Kontrollmechanismen früh mitzudenken. Die Teilnahme hilft dabei, regulatorische Anforderungen in technische Entscheidungen zu übersetzen. Sie ersetzt keine Rechtsberatung, schafft aber eine bessere Grundlage für die Umsetzung im Engineering.

Wie verhinderst du, dass mehr AI-Code zu mehr Review- und Rework-Kosten führt?

Agenten können Code schneller erzeugen, als Teams ihn manuell prüfen können. Ohne geeignete Quality Gates verschiebt sich der Produktivitätsgewinn deshalb schnell in Review-Backlogs, technische Schulden und zusätzliche Fehlerkosten.

Die W-JAX behandelt genau diese zweite Hälfte der AI-Produktivitätsfrage: Wie wird aus mehr Output auch tatsächlich mehr Lieferfähigkeit?

Wenn die Vibes nicht reichen: Observability und Evaluation für verlässlichere GenAI-Anwendungen

Statt Ergebnisse nur subjektiv zu bewerten, geht es um messbare Kriterien und systematische Fehleranalyse. Das verkürzt Feedbackschleifen zwischen Prototyp, Test und produktivem Betrieb.

Architektur als ausführbarer Vertrag – Wie Coding Agents Ihre Frontend-Architektur respektieren

Architekturregeln müssen für Coding Agents nicht nur dokumentiert, sondern überprüfbar werden. Ausführbare Regeln, Skills und Architekturchecks helfen dabei, die Codebasis auch bei höherem Änderungstempo konsistent zu halten.

KI diktiert aber Du haftest: Dein Schutzschild im Code Generierungs-Dschungel

AI-generierter Code entbindet Teams nicht von ihrer Verantwortung. Die Session richtet den Blick deshalb auf Kontrollmechanismen, mit denen Qualität und Risiken früh im Entwicklungsprozess sichtbar werden.

FeatureOps Workshop: From First Flag to Runtime Control

Feature Flags, progressive Rollouts und Kill Switches schaffen zusätzliche Kontrolle im Betrieb. Fehlerhafte oder riskante Änderungen lassen sich gezielter begrenzen, statt direkt einen vollständigen Rollback oder Incident zu provozieren.

Argument für deine Führungskraft: Der wirtschaftliche Nutzen von AI entsteht nicht durch mehr Codezeilen. Er entsteht dann, wenn Review-, Rework- und Incident-Kosten nicht im gleichen Maß wachsen. Genau dafür liefert das Programm konkrete Engineering-Ansätze.

Wie modernisierst du bestehende Systeme ohne Big Bang?

Der größte Softwarewert vieler Unternehmen steckt nicht in neuen Greenfield-Projekten, sondern in bestehenden Systemen, die täglich weiterlaufen müssen. Modernisierung darf deshalb nicht nur technisch funktionieren. Sie muss auch kontrollierbar, testbar und im laufenden Betrieb umsetzbar sein.

Das W-JAX-Programm behandelt mehrere Ansätze, mit denen Teams Legacy- und Brownfield-Systeme schrittweise weiterentwickeln können.

Legacy Software mit Strangler Pattern ablösen

Das Strangler Pattern ermöglicht eine schrittweise Ablösung bestehender Systeme. Neue und alte Komponenten können übergangsweise parallel betrieben werden, während Funktionen und Daten kontrolliert migriert werden.

AI Meets Legacy, Desktop-to-Web Modernization Without Interrupting the Business

Die Session zeigt einen pragmatischen Modernisierungsweg für bestehende Anwendungen, bei dem Migration, Build-Schleifen und Rollback-Möglichkeiten so kombiniert werden, dass der Geschäftsbetrieb nicht für einen Big-Bang-Rewrite unterbrochen werden muss.

LLM-Assisted Migrations and Modernizations: from Museum to Mainstream

AI kann bei Refactoring, Testabdeckung, Dokumentation und Wissensaufbau unterstützen. Entscheidend ist dabei ein inkrementelles Vorgehen, das Veränderungen überprüfbar hält, statt die gesamte Codebasis in einem Schritt neu zu schreiben.

Agentic Software Modernization: Back to the Roots

Coding Agents brauchen im Brownfield klare Grenzen. Seams, Slicing, Anker und Fitness Functions helfen dabei, ihren Aktionsraum so einzuschränken, dass Modernisierung in kleineren und besser kontrollierbaren Schritten stattfinden kann.

Argument für deine Führungskraft: Der Nutzen der W-JAX liegt nicht nur darin, neue Technologien kennenzulernen. Genauso wichtig ist die Frage, wie bestehende Softwareinvestitionen geschützt und Modernisierung planbarer gemacht werden können.

Warum lohnt sich die W-JAX gegenüber reinem Selbststudium?

Viele der behandelten Themen lassen sich natürlich auch über Dokumentationen, Videos, Blogposts und eigene Experimente erschließen. Das kostet jedoch Zeit und liefert häufig nur einzelne Perspektiven.

Auf der W-JAX kannst du innerhalb weniger Tage unterschiedliche Ansätze aus Enterprise AI, Java, Software-Architektur, Security und Modernisierung vergleichen. Du bekommst nicht nur Erfolgsbeispiele, sondern auch Grenzen, Gegenpositionen und Erfahrungswerte aus realen Projekten. Gerade bei Entscheidungen mit langfristigen Auswirkungen kann diese Verdichtung wertvoller sein als die nächste einzelne Tool-Demo.

Dazu kommen Workshops, in denen du Methoden und Setups praktisch ausprobieren kannst, sowie der direkte Austausch mit Speaker:innen und anderen Teilnehmer:innen.

So machst du aus deiner Teilnahme einen konkreten Business Case

Je konkreter du den Nutzen für dein Unternehmen formulierst, desto leichter lässt sich die Teilnahme intern begründen. Statt nur auf einzelne Talks zu verweisen, kannst du deinen Antrag an drei Fragen ausrichten:

  • Welche aktuellen Herausforderungen haben wir? Zum Beispiel AI-Einführung, Security, Compliance, Legacy-Modernisierung oder steigende Review-Kosten.
  • Welche Sessions oder Workshops helfen uns dabei? Wähle gezielt die Programmpunkte aus, die zu deinen Projekten passen.
  • Wie kommt das Wissen zurück ins Team? Plane zum Beispiel ein Recap, einen internen Tech Talk oder ein kleines Pilotprojekt.

So wird aus einer Konferenzreise ein klarer Weiterbildungsvorschlag mit fachlichem Ziel und nachvollziehbarem Wissenstransfer.

The post W-JAX 2026: So überzeugst du deinen Chef appeared first on JAX.

]]>
Warum jetzt die beste Zeit ist, Entwickler:in zu sein https://jax.de/blog/ai-softwareentwicklung-entwickler-disziplin/ Mon, 24 Aug 2026 10:31:28 +0000 https://jax.de/?p=210770 AI schreibt Code in Sekunden. Das ist nicht nur beunruhigend, sondern gefährlich! Venkat Subramaniam, der auf der W-JAX 2026 eine Keynote halten wird, bringt es auf einen Nenner: Nicht die Programmierung entscheidet über gute Software, sondern die Disziplin der Entwickler:innen, die sie schreiben. Was das bedeutet, zeigt er an drei sehr konkreten Geschichten – einer Klippe, einem Zehn-Minuten-Prototyp und einer Zahl: siebzig Prozent.

The post Warum jetzt die beste Zeit ist, Entwickler:in zu sein appeared first on JAX.

]]>

Hinweis: Dieser Podcast und dieses Video wurden mithilfe von KI erstellt. Dabei wurden die Originalinhalte und technischen Erkenntnisse des Autors des Blogbeitrags adaptiert.

 

Vor dreißig Jahren saß Venkat Subramaniam nach einer Gletschertour in Kanada im Bus zurück ins Tal. Der Fahrer unterbreitete seinen Passagieren zwei Vorschläge: zwei Stunden über die Straße fahren. Oder zwanzig Sekunden – über die Klippe. Die Gruppe entschied sich, wenig überraschend, für den Weg über die Straße.

Genau dort steht die Softwareentwicklung heute, sagt Subramaniam. AI erzeugt in zwanzig Sekunden eine Menge Code. Aber nicht jeder schnelle Weg ist auch ein guter.

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Nicht Entwickler, sondern Problemlöser

Subramaniam ist beileibe keine beliebige Stimme in der AI-Debatte. Vor allem als Trainer hat er weltweit mehrere Generationen von Entwicklern aus- und weitergebildet, dazu als Buchautor und gefragter Keynote-Speaker. Seit vierzig Jahren beantwortet er Fragen, die vielen eher nur lästig sind: warum eine Methode zu lang läuft, warum ein Name seine volle Bedeutung tragen muss, warum ein Test mehr sagen sollte als nur „grün”.

Seine bekannteste Unterscheidung: Der Anfänger fragt, welchen Code er schreiben kann. Der Experte fragt, welchen Code er vermeiden kann – denn Code ist eine Verbindlichkeit, kein Vermögenswert. Für viele in der Community ist Venkat Subramaniam so etwas wie ein Vorbild.

„Wer bist du?” ist für ihn eine Fangfrage. Die falsche Antwort: Java-Entwickler, Python-Entwickler, Kotlin-Entwickler. Seine eigene Antwort lautet anders: Wir sind nicht Programmierer, sondern Problemlöser. Programmieren ist eines unserer Werkzeuge.

Deshalb überrascht seine Reaktion auf AI. Kein Bedauern, dass jetzt eine Maschine codet. Kein „das schafft sie nie so gut wie ich”. Subramaniam beschreibt sich selbst als fasziniert von den neuen Möglichkeiten, denn: Jeder Moment im Leben ist eine Gelegenheit zu lernen. Er hat auch eine Erklärung dafür, warum andere das anders empfinden mögen: Wer eine Aufgabe lange auf dieselbe Weise erledigt hat, fühlt sich bedroht, wenn ein Werkzeug diesen Teil übernimmt – das eigene Revier gerät ins Wanken. Aber das Werkzeug nimmt nur das weg, was nicht das Entscheidende ist. Sein eigenes Ziel, sagt Subramaniam, habe sich nicht verändert. Er habe nur ein deutlich mächtigeres Werkzeug bekommen, um es zu erreichen.

Live sehen kann man ihn bald selbst: Venkat Subramaniam hält die Eröffnungs-Keynote auf der kommenden W-JAX – „Worth a Million Arguments”, darüber, wie man technische Grundsatzfragen entscheidet, wenn jede Option gleich gut klingt.

Geschwindigkeit ohne Disziplin ist kein Fortschritt

„Geschwindigkeit plus Disziplin ist Agilität. Geschwindigkeit ohne Disziplin ist eine Katastrophe!” sagte Subramaniam und es klingt fast wie ein Slogan. Aber dieser Satz basiert auf einer Beobachtung aus vierzig Jahren Praxis – und sie trifft die Arbeit mit AI direkt: AI liefert die Geschwindigkeit. Was daraus wird, entscheidet, wer sie einsetzt.

Subramaniam hat Entwickler jahrzehntelang in Java, Kotlin und anderen Sprachen trainiert – und sagt trotzdem: Im Kern geht es nicht um die Sprache. Es geht um eine Haltung. Teams wollten schon immer das Richtige tun, ihnen fehlt meistens nur die Zeit. Man kennt zwar den Standard, den man erreichen sollte, lässt ihn unter Termindruck aber fallen. Zum ersten Mal, sagt Subramaniam, können Teams schnell sein und trotzdem richtig arbeiten. Für Entwickler ist das die Chance, versäumte Disziplin nachzuholen.

„Du kannst Entwicklung an die AI auslagern. Deinen eigenen Ruf kannst du nicht auslagern”, sagt er.

Disziplin zeigt sich im Bauen, nicht im Reden

Wie das in der Praxis aussieht, zeigt folgende Episode: Ein Kunde wollte ein bestimmtes Feature. Subramaniams Bauchgefühl: keine gute Idee. Aber wie sagt man das, bevor man es getestet hat? Das wäre nur eine Vermutung gewesen – ein Bias, wie er selbst sagt. Also bedankte er sich für die Idee und baute sie mit AI in zehn Minuten als funktionierenden Prototyp. Der Kunde sah sich das an. Innerhalb weniger Minuten waren sich beide einig: wir verwerfen die Idee ganz schnell.

Software zeigt, sagt Subramaniam, den „Beobachter-Effekt”: In dem Moment, in dem man etwas Konkretes ansieht, will man es ändern. Diesen Moment des Sehens herzustellen, kostete früher Tage oder Wochen Prototyping. Heute kostet er zehn Minuten – das Software-Äquivalent zum 3D-Druck im Maschinenbau.

Ein mächtigeres Werkzeug macht den Job nicht obsolet

Wenn AI uns also so produktiv macht, warum dann nicht die Belegschaft kürzen? Subramaniam ist überzeugt, dass es nicht so kommen wird. Diesen Film hat er schon zweimal gesehen.

Das erste Mal war Offshoring, Anfang der 2000er. Westliche Softwareentwicklung galt als erledigt – warum teuer im eigenen Land produzieren, wenn es anderswo billiger geht? Offshoring fing echte Kapazitätsspitzen auf. Dabei lernte die Branche aber etwas, das sie nur geahnt hatte: Softwareentwicklung ist Kommunikation, Verständnis, Iteration – kurze Wege zu Kunden und Stakeholdern. Diese Erkenntnis bekam einen Namen: Agile. Am Ende brauchten wir mehr Entwickler:innen als je zuvor.

Die zweite Erinnerung ist jünger: die Automatisierung des Testens. Wer damals glaubte, automatisierte Tests würden professionelle Tester überflüssig machen, lag gründlich falsch. Automatisierung hat das Testen als Aufgabengebiet nicht verringert, sondern im Gegenteil größer gemacht – systemischer, komplexer, anspruchsvoller. Der Job wuchs. Er verschwand nicht.

Subramaniam zieht daraus einen einfachen Schluss für AI in der Softwareentwicklung: Wer glaubt, ein mächtigeres Werkzeug mache den Job obsolet, hat die Geschichte nicht verstanden.

Beides zählt: Grundlagen und kritisches Denken

Was braucht es also, wenn nicht die Kenntnis einer Programmiersprache? Subramaniam antwortet mit zwei Wörtern, die er in fast jedem Satz wiederholt: Grundlagen und kritisches Denken. Grundlagen heißt: verstehen, was Software im produktiven Einsatz wirklich braucht – Performance, Sicherheit, Zuverlässigkeit, Resilienz. Kritisches Denken heißt: bei jedem AI-Ergebnis fragen, ob es tatsächlich Sinn ergibt, und ob es einen besseren Weg gibt.

Ein Projektmanager fragte ihn einmal, ob Grundlagen in der Softwareentwicklung überhaupt noch nötig seien, wenn AI doch den Rücken freihalte. Subramaniams Antwort: AI hält niemandem den Rücken frei. Ganz im Gegenteil: Sie stellt dich öffentlich bloß, auf die denkbar peinlichste Weise, die möglich ist. AI ersetzt Wissen nicht – ersetzbar ist nur die Ausführung, nicht das Wissen selbst.

70 Prozent der Arbeit ist Lesen, nicht Schreiben

Wer verstehen will, worauf Subramaniam hinaus will, sollte sich eine Zahl merken, die er gerne zitiert: Rund siebzig Prozent der Arbeit mit Code ist Lesen, nur dreißig Prozent Schreiben. AI ist genau dort am stärksten, wo Menschen am schwächsten sind – bei kognitiver Last, bei Komplexität, die sich nicht auf einen Blick erfassen lässt. Das Tempo übernimmt die Maschine. Die Richtung bestimmt weiterhin der Mensch.

Man müsse immer eine Abstraktionsebene unter der eigenen verstehen, so Subramaniam, sonst lässt sich nicht beurteilen, wann etwas kaputtgeht. Nicht, um die AI zu beherrschen, sondern um das eigene Urteilsvermögen zu schärfen, während AI den Code schreibt.

Vertraue nicht, und prüfe es bis zum Anschlag

Gefragt, wonach er das Können eines Engineers bewertet, antwortet Subramaniam ohne zu zögern. Die alte Norm war „trust but verify” – vertrauen, aber prüfen. Seine neue Norm liest sich deutlich härter: „Vertraue nicht, und prüfe bis zum Anschlag.” Sein Ziel bringt er in einem Satz auf den Punkt: „Wir wollen nachhaltige Geschwindigkeit, keine unkontrollierte.”

Genau darum geht es in seiner Keynote auf der W-JAX, „Worth a Million Arguments: Making Better Decisions in the Age of AI”. Spring, Quarkus oder Micronaut? Angular, React oder Vue? Statische, dynamische oder gemischte Typisierung – oder doch funktional? Fragen, die in jedem Team endlose Diskussionen auslösen, sobald erfahrene Entwickler im selben Raum sitzen. Prototypen, die helfen, solche Diskussionen anschaulich zu führen, kosteten bis vor Kurzem sehr viel Zeit und Aufwand. Heute lassen sie sich in Windeseile bauen, beobachten, vergleichen.

Subramaniam zieht dafür Parallelen zur Praxis bewunderter Künstler, Wissenschaftler und Architekten: Sie streiten nicht über Skizzen – sie bauen Modelle und stellen sie nebeneinander. Aus der Debatte über Spring oder Quarkus wird so der Vergleich zweier funktionierender Prototypen. Schneller experimentieren macht das technische Urteilsvermögen dabei nicht überflüssig: Zu wissen, was man überhaupt bauen sollte, worauf man achten muss und was ein Ergebnis wirklich bedeutet, bleibt Erfahrungssache.

Die beste Zeit, um Entwickler:in zu sein

AI verändert einiges daran, wie Software entsteht. Die simple Sichtweise behandelt das als vollständige Automatisierung – Problem eingeben, Software erhalten, Mensch optional. Subramaniam verteidigt die Grundlagen seit vierzig Jahren, genau weil sie nicht optional werden, nur weil eine Maschine schneller geworden ist. Sein Schluss aus vier Jahrzehnten Praxis: Ein mächtigeres Werkzeug macht den Job nicht unbedeutender. Es macht ihn anspruchsvoller.

Das ist die eigentliche Nachricht, und sie ist gut: Die Prinzipien – Disziplin, Grundlagen, kritisches Denken – zählen jetzt mehr denn je. Nicht trotz AI, sondern wegen ihr.

Genau dafür steht die W-JAX. Subramaniam ist nicht da, um ein Werkzeug zu erklären. Er ist da, um die Ingenieurskultur mitzugestalten, die dieser Moment verlangt.

The post Warum jetzt die beste Zeit ist, Entwickler:in zu sein appeared first on JAX.

]]>
Project Leyden: AOT-Caching für Java & Spring Boot https://jax.de/blog/project-leyden-aot-caching-java-spring-boot/ Thu, 20 Aug 2026 10:38:40 +0000 https://jax.de/?p=210747 Project Leyden bringt AOT-Caching auf die Standard-JVM und verkürzt Start-up- und Warm-up-Zeiten von Java- und Spring-Boot-Anwendungen. Der Artikel zeigt die Workflows mit JDK 24 bis 26, die Integration in Container und CI/CD sowie wichtige Regeln für Training, Cache-Validierung und Security.

The post Project Leyden: AOT-Caching für Java & Spring Boot appeared first on JAX.

]]>
AOT-Caching ist 2026 kein Experiment mehr. Mit JDK 24 bis 26 gibt es einen belastbaren Pfad, um Java-Anwendungen schneller in einen produktiven Zustand zu bringen, ohne dabei die Kompatibilität und Dynamik der JVM aufzugeben. Project Leyden verlagert wiederholbare JVM-Arbeit in einen kontrollierten Trainings- und Build-Kontext und stellt sie als Cache zur Laufzeit bereit. Java hat in den letzten Jahrzehnten vieles richtig gemacht: Es steht für eine robuste Laufzeit, ein reifes Ökosystem und eine bemerkenswerte Kompatibilität über Plattformen und Sprachversionen hinweg. Dabei fiel eine Schwäche lange kaum ins Gewicht: der vergleichsweise aufwendige Startpfad. In klassischen Betriebsmodellen liefen Anwendungen stabil über Wochen oder Monate durch. Ob der Start zwanzig oder sechzig Sekunden dauerte, war oft zweitrangig.

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

In cloud-nativen Architekturen ist diese Sichtweise nicht mehr tragfähig. Instanzen werden häufiger neu gestartet, horizontal skaliert, geupdatet und gezielt ersetzt. Bei Lastspitzen ist entscheidend, wie schnell eine neue Instanz wirklich nutzbar wird. Im Fehlerfall bestimmt der Wiederanlauf über die Verfügbarkeit, und bei kurzen Lebenszyklen schmälert jede unproduktive Startphase direkt das Infrastrukturbudget. Somit ist Start-up-Zeit nicht mehr nur eine technische, sondern auch eine betriebswirtschaftliche und ökologische Größe.

Genau diesen Punkt adressiert Project Leyden [1]. Leitmotiv ist, Berechnungen zeitlich nach vorn zu verlagern: Was sonst bei jedem Start erneut stattfinden würde, wird einmalig in einem Trainings- und Build-Kontext beobachtet, als Artefakt gesichert und zur Laufzeit wiederverwendet. Der Ansatz ersetzt die JVM nicht und führt auch nicht zu einem Closed-World-Modell wie GraalVM Native Image. Das dynamische Verhalten der JVM bleibt erhalten, während Start- und Warm-up-Pfade gezielt entlastet werden.

AOT-Caching eignet sich somit besonders für Teams, die schnelle, reproduzierbare Verbesserungen auf der Standard-JVM erzielen möchten, ohne die Plattform zu wechseln. Der vorliegende Artikel erläutert die technischen Grundlagen sowie die wichtigsten Workflows für Java und Spring Boot und stellt die Betriebsregeln vor, die einen Cache von einem netten Experiment in ein belastbares Produktionsartefakt verwandeln.

Start-up ist heute ein Betriebsproblem

Um den Nutzen von AOT-Caching realistisch bewerten zu können, ist es zunächst wichtig, eine saubere Messung durchzuführen. In der Praxis hat sich die Unterscheidung zweier Messpunkte bewährt. Erstens der Zeitpunkt tready, ab dem eine Instanz die erste sinnvolle Anfrage stabil bedient. Zweitens der Zeitpunkt tsteady, ab dem Durchsatz und Latenz unter Last in einem stabilen Bereich liegen. Diese Unterscheidung ist essenziell, denn ein erfolgreicher Health-Check sagt noch nichts über den Lastzustand aus. Viele Anwendungen melden sich frühzeitig bereit, obwohl sie intern noch mitten im Warm-up stecken.

Das ökonomische Argument folgt unmittelbar. Eine Instanz, die bereits Ressourcen belegt, aber noch nicht produktiv arbeitet, erzeugt Leerkosten. Gleichzeitig steigt der Energiebedarf während Start-up und Warm-up, ohne dass in dieser Zeit bereits ein proportionaler Nutzwert entsteht. Bei vielen Tausend Container-Starts pro Tag summiert sich das spürbar – sowohl auf der Cloud-Rechnung als auch im CO2-Profil der Plattform. Abbildung 1 fasst die typische Sequenz vom kalten Start bis zur stabilen Lastphase zusammen und macht die beiden Messpunkte sichtbar.

Abb. 1: Vom kalten Start bis zur stabilen Lastphase

Für Teams ergibt sich daraus die Pflicht, jede Optimierungsvariante mit demselben Mess-Set zu bewerten. Dabei sollten mindestens tready, tsteady, das Speicherprofil und ein reproduzierbares Lastszenario dokumentiert sein. Erst dann lassen sich Exploded JAR, Class Data Sharing, AOT-Cache und gegebenenfalls Native Image sinnvoll vergleichen. Ohne diese Baseline bleibt jeder Speed-up eine gefühlte Wahrheit.

Konkrete Größenordnungen helfen bei der Einschätzung der Erwartungen. Das Spring-Team berichtet für die bekannte PetClinic-Anwendung von Start-up-Zeiten, die sich durch CDS und AOT-Cache von rund drei auf etwa eine Sekunde reduzieren – ein Faktor von zwei bis drei, der für klassische Spring-Boot-Services typisch ist. Bei Plain-Java-Anwendungen ist der relative Effekt geringer, da weniger Klassenlade- und Konfigurationsarbeit eingespart werden kann. In Microservice-Flotten mit vielen kurzlebigen Instanzen ist nicht der einzelne Lauf der eigentliche Gewinn, sondern die Summe über Tausende Container-Starts pro Tag.​

Dabei ist weniger die konkrete Zahl entscheidend als vielmehr die Reproduzierbarkeit der Messmethode. Jedes Team sollte die Baseline auf den eigenen Workloads ermitteln, statt sich auf fremde Benchmarks zu verlassen.

Project Leyden ist kein einzelnes Feature, sondern ein mehrjähriges OpenJDK-Programm. Die Entwicklung lässt sich an vier JEPs gut nachvollziehen: JEP 483 in JDK 24 bringt Ahead-of-Time Class Loading and Linking [2], JEP 514 in JDK 25 verbessert die Ergonomie auf der Kommandozeile [3], JEP 515 ergänzt Ahead-of-Time Method Profiling – die Profiling-Daten heißer Methoden werden direkt im AOT-Cache gespeichert (es werden keine separate Datei, keine zusätzlichen Flags benötigt); der JIT-Compiler kann diese Daten beim Start sofort konsumieren [4] – und JEP 516 in JDK 26 schließlich hebt die GC-Einschränkung beim Object Caching auf und öffnet den Weg für ZGC [5]. Die Botschaft ist klar: AOT-Caching ist kein exotischer Sonderpfad, sondern ein graduell gereiftes JVM-Feature mit konkretem Betriebsnutzen. Tabelle 1 fasst den Funktionsumfang pro JDK-Release zusammen.

JDK Neue Fähigkeit Praktischer Effekt
24 JEP 483 – AOT Class Loading & Linking Drei-Phasen-Workflow mit AOTMode=record/create
25 JEP 514 – CLI-Ergonomie Ein-Schritt-Training mit AOTCacheOutput
25 JEP 515 – Method Profiling JIT erhält Profil-Daten schon beim Start
26 JEP 516 – Object Caching mit beliebigem GC AOT-Cache auch mit ZGC nutzbar

Tabelle 1: Leyden-Fähigkeiten pro JDK-Release

Gleichzeitig ist AOT-Caching weiterhin Teil eines Werkzeugkastens. Exploded JARs, CDS [10], AOT-Cache, CRaC und Native Image weisen unterschiedliche Stärken und Grenzen auf. Es geht nicht um Glaubensfragen, sondern um Trade-offs pro Workload. Tabelle 2 ordnet die wichtigsten Verfahren ein und macht deutlich, warum AOT-Caching für viele Teams der pragmatischste nächste Schritt ist.

Ansatz Stärken Grenzen Typischer Einsatz
Exploded JAR sehr niedrige Einstiegshürde, schnellerer Classpath-Start kein tieferes Runtime-Tuning schneller Quick-Win
CDS etabliert, robust, gut verstanden begrenzter Optimierungsumfang konservative Produktionsumgebungen
AOT-Cache guter Kompromiss aus Nutzen und Kompatibilität erfordert diszipliniertes Training cloud-native JVM-Workloads
CRaC extrem schneller Restore Snapshot- und Betriebskomplexität, Linux/CRIU Spezialfälle mit harten Start-up-Zielen
Native Image sehr schneller Kaltstart, geringer Footprint Closed-World, höherer Build-Aufwand Serverless- und CLI-Szenarien

Tabelle 2: Einordnung gängiger Start-up-Strategien

Tabelle 1 zeigt zugleich eine andere wichtige Eigenschaft: Jedes Release baut additiv auf dem vorherigen auf. Wer heute mit JDK 24 startet, hat einen klaren Upgrade-Pfad in Richtung JDK 25 und 26, ohne den Workflow grundlegend umstellen zu müssen.

Project Leyden: das Prinzip hinter dem AOT-Cache

Das eigentliche Prinzip von Leyden ist einfach formuliert, seine Wirkung jedoch weitreichend. Statt jeden Programmstart als isoliertes Ereignis zu behandeln, wird die JVM einmalig in einem repräsentativen Lauf beobachtet. Welche Klassen werden geladen, welche werden gelinkt, welche Methoden sind heiß? Die Antworten auf diese Fragen wandern in eine Konfiguration und anschließend in einen Cache. Spätere Starts können diesen Cache laden und überspringen damit große Teile der Arbeit, die sonst bei jedem Hochfahren neu anfallen würde.

Diese Mechanik ähnelt nicht zufällig der JIT-Welt. Auch dort beobachtet die Laufzeit reales Verhalten und nutzt diese Informationen, um später schneller zu sein. Der Unterschied liegt in der zeitlichen Verlagerung: Während JIT zur Laufzeit lernt, erfolgt das Lernen beim AOT-Cache bereits zur Build-Zeit – allerdings auf Basis echter Beobachtungen und nicht ausschließlich durch statische Analyse. Genau diese Kombination unterscheidet Leyden von Ansätzen wie GraalVM Native Image, die auf einer geschlossenen Weltannahme aufbauen und daher auf einen Teil der Java-Dynamik verzichten müssen.

Aus diesem Ergebnis lässt sich die folgende Linie für Architekturentscheidungen ableiten. Wer schnellen Start-up ohne Verlassen der JVM wünscht, ist beim AOT-Cache richtig. Wer dagegen einen Millisekunden-Kaltstart und einen minimalen Footprint benötigt und die Closed-World-Einschränkungen akzeptiert, hat mit Native Image weiterhin die passende Lösung. Beide Ansätze sind komplementär und schließen einander nicht aus. Der AOT-Cache ist häufig der erste Schritt, weil er ohne Migration auskommt.

Wie AOT-Caching technisch funktioniert

Drei-Phasen-Workflow als robuste Basis

Der fachliche Kern besteht aus drei Schritten: Zunächst werden Beobachtungen aus einem Trainingslauf als Konfiguration festgehalten. Daraus wird ein Cache erzeugt, der beim späteren Start geladen wird. Praktisch wichtig ist die Trennung dieser Schritte, da sie die Reproduzierbarkeit und Fehlersuche erheblich vereinfacht. Mit JDK 24 war der explizite Drei-Phasen-Workflow, wie ihn Listing 1 zeigt, der Normalfall. Er ist auch heute noch relevant, wenn Training und Assembly in unterschiedlichen Build-Umgebungen stattfinden oder wenn Teams den Prozess granular steuern müssen.

Listing 1: Drei-Phasen-Workflow (Record, Create, Runtime)

# Phase 1: Training/Recording
java -XX:AOTMode=record \
     -XX:AOTConfiguration=build/app.aotconf \
     -cp build/app.jar com.example.App

# Phase 2: Assembly/Cache creation
java -XX:AOTMode=create \
     -XX:AOTConfiguration=build/app.aotconf \
     -XX:AOTCache=build/app.aot \
     -cp build/app.jar

# Phase 3: Deployment/Runtime
java -XX:AOTCache=build/app.aot \
     -cp build/app.jar com.example.App

Zwei-Phasen-Ergonomie ab JDK 25

Ab JDK 25 wird der Ablauf mit AOTCacheOutput ergonomischer, weil Training und Assembly in einem einzigen Aufruf zusammengeführt werden können [3]. Listing 2 zeigt diese Kurzform. Dadurch wird die Zahl der Kommandos reduziert und die Einstiegshürde im lokalen Entwicklungsalltag spürbar gesenkt. Die fachlichen Regeln bleiben dabei unverändert. Trainingsqualität und Konsistenz der Umgebung entscheiden weiterhin über den realen Nutzen.

Listing 2: Zwei-Phasen-Workflow (JDK 25+)

# Training und Assembly in einem Schritt
java -XX:AOTCacheOutput=build/app.aot \
     -cp build/app.jar com.example.App

# Deployment
java -XX:AOTCache=build/app.aot \
     -cp build/app.jar com.example.App

Der zweistufige Pfad startet intern einen Sub-Prozess mit einer ähnlichen Heap-Größe wie die ursprüngliche JVM. Das ist auf Entwicklungsrechnern unkritisch, kann in CI-Runnern mit knappen Speicherlimits jedoch zu Engpässen führen. In solchen Umgebungen lohnt der bewusste Rückgriff auf den Drei-Phasen-Weg. Die Regel lautet: Der komfortable Pfad ist nicht automatisch der robusteste für jede Pipeline. Wer reproduzierbar deployen möchte, sollte die Build-Variante wählen, die zum eigenen CI/CD-Profil passt, und sich nicht nur auf die kürzeste Kommandozeile konzentrieren.

Verifikation und striktes Laden

Für den Betrieb ist Verifikation Pflicht. Zwei Schalter helfen unmittelbar: ein strikter Modus, der einen unbrauchbaren Cache als Fehler meldet, sowie ein aussagekräftiges Logging, das das Laden von Klassen und das AOT-Verhalten sichtbar macht. Listing 3 zeigt beide Mechanismen nebeneinander. Wichtig: -XX:AOTMode=on ist laut JEP 483 ausdrücklich als Diagnose-Schalter gedacht und sollte in Produktion vermieden werden, da vom Cloud-Anbieter zugeschaltete JVMTI-Agents den Cache unter Umständen invalidieren und den Start fehlschlagen lassen können [2]. Der richtige Ort für den strikten Modus ist die CI/CD-Pipeline und das Staging. In Produktion bleibt es bei -XX:AOTCache=…, ergänzt um Logging und Metriken, die einen stillschweigend deaktivierten Cache schnell sichtbar machen.

Ein Detail zum Logging: Das früher übliche -Xlog:cds ist mit dem AOT-Cache in -Xlog:aot aufgegangen. CDS-Schalter bleiben aus Kompatibilitätsgründen funktional, neue Diagnosen sollten aber die aot-Tags verwenden.

Listing 3: Strikter Modus und log-basierte Verifikation

# Fehler, wenn der Cache nicht nutzbar ist
java -XX:AOTCache=build/app.aot \
     -XX:AOTMode=on \
     -cp build/app.jar com.example.App

# Sichtbarkeit für Ladepfad und AOT-Verhalten
java -XX:AOTCache=build/app.aot \
     -Xlog:aot=info,class+load=info \
     -cp build/app.jar com.example.App

Spring Boot: vom Konzept zur produktiven Routine

Extracted Layout statt Fat JAR

In Spring-Boot-Systemen entfaltet AOT-Caching seinen Nutzen, sobald die Prozesskette steht: Artefaktstruktur vorbereiten, Training reproduzierbar durchführen, Runtime mit Cache starten und den Effekt sauber messen. Der erste und häufigste Stolperstein ist das Packaging. Ein klassisches Fat JAR ist für den AOT-Cache-Pfad ungünstig, weil verschachtelte JAR-Strukturen sowie der interne Zugriffspfad die Cache-Anforderungen erschweren. Der produktive Weg beginnt deshalb mit dem extrahierten Layout. Spring Boot stellt dafür seit Version 3.3 einen offiziellen Tool-Modus bereit, der das Fat JAR in eine flache Struktur aus Anwendungs-JAR und einem lib-Verzeichnis zerlegt. Die hier gezeigte AOT-Cache-Workflow-Dokumentation gilt ab Spring Boot 3.5 [6], [8]. Listing 4 zeigt den passenden Aufruf.

Listing 4: Spring-Boot-Artefakt extrahieren

java -Djarmode=tools \
     -jar target/aot-cache-demo.jar \
     extract --destination extracted

Trainingslauf mit Exit-on-Refresh

Für den Trainingslauf hat sich in vielen Teams folgende pragmatische Strategie etabliert: Die Anwendung wird bis zum Context-Refresh gestartet und dann gezielt beendet. In Spring Boot lässt sich dieses Verhalten mit der Property spring.context.exit=onRefresh sauber steuern [6]. Der Lauf instanziiert alle nicht-lazy Singletons, ruft afterPropertiesSet auf, durchläuft den Lifecycle bis zum ContextRefreshedEvent und beendet sich anschließend kontrolliert. Damit deckt der Trainingsschritt den vollständigen Start-up-Pfad ab, ohne externe Abhängigkeiten wie eine produktive Datenbank anzusprechen. Listing 5 fasst den Aufruf zusammen.

Listing 5: Reproduzierbarer Trainingslauf für Spring Boot

cd extracted
java -XX:AOTCacheOutput=app.aot \
     -Dspring.context.exit=onRefresh \
     -jar aot-cache-demo.jar

Runtime und Spring AOT

Wie Listing 6 zeigt, bleibt die Runtime-Phase bewusst schlicht. Das ist ein Vorteil im Betrieb, da die Startlogik nicht mit Spezialcode angereichert wird, sondern allein über JVM-Parameter gesteuert wird. Dadurch bleibt der Anwendungscode frei von Cache-Logik und die Cache-Aktivierung ist eine reine Deploymententscheidung.

Listing 6: Start mit aktivem AOT-Cache

cd extracted
java -XX:AOTCache=app.aot \
     -Xlog:aot=info \
     -jar aot-cache-demo.jar

Ein häufiges Missverständnis in Projekten ist die Gleichsetzung von Spring AOT und JVM AOT-Cache. Wie Tabelle 3 gegenüberstellt, arbeiten beide auf unterschiedlichen Ebenen mit unterschiedlichen Zielen. Spring AOT ist eine Build-Time-Optimierung im Framework, bei der Reflection-Hints generiert, Bean-Definitionen vorbereitet und Konfigurationsklassen aufgelöst werden. Der JVM AOT-Cache wirkt hingegen unterhalb des Frameworks und optimiert die Arbeit innerhalb der HotSpot-Laufzeit selbst. In Kombination liefern beide Ansätze regelmäßig den besten Effekt. Wer von der JVM auf GraalVM Native Image wechseln möchte, ist auf Spring AOT angewiesen. Auf der klassischen JVM ist es ein Add-on, das selektiv aktiviert werden kann [6], [7].

Aspekt AOT-Cache Spring AOT
Ebene JVM / HotSpot Spring Framework
Primäres Ziel Start-up und Warm-up der JVM verbessern Build-time-Vorbereitung von Framework-Arbeit
Pflicht für Native Image nein ja
Kombinierbar ja ja

Tabelle 3: Unterschiedliche Ebenen, kombinierbarer Nutzen

Für Teams ergibt sich daraus eine klare Reihenfolge beim Rollout. Zuerst Baseline messen, dann AOT-Cache in die Build-Pipeline integrieren, anschließend prüfen, ob zusätzliches Spring AOT in der konkreten Anwendung den erwarteten Mehrwert liefert. Diese Schrittfolge reduziert das Risiko und macht die Wirkung jeder einzelnen Maßnahme nachvollziehbar.

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Trainingsläufe richtig gestalten

Die Qualität des Caches steht und fällt mit der Qualität des Trainings. Ein Cache, der nur triviale Pfade durchlaufen hat, liefert in Produktion wenig Nutzen. Umgekehrt erzeugt ein überfrachtetes Training unnötig große Artefakte und verlangsamt den Build, ohne dass die zusätzlichen Klassen und Methoden in der Realität benötigt werden. Das Ziel ist ein repräsentatives, aber fokussiertes Trainingsprofil. In Bestandsprojekten lohnt es sich, drei bis fünf kritische User-Journeys zu identifizieren und sie als Trainings-Harness zu kodifizieren.

In der Praxis haben sich zwei Strategien etabliert. Die erste ist der schlanke Lifecycle-Run mit Exit-on-Refresh. Er ist schnell, stabil in CI und liefert vor allem Klassenlade-Daten für den Start-up-Pfad. Die zweite Strategie ist ein Smoke- oder Integrationsprofil, das gezielt zentrale API-Pfade anfährt. Damit gewinnt man auch Profiling-Informationen über die heißen Methoden des Warm-up-Pfads. Das kann insbesondere ab JDK 25 mit Method Profiling den Unterschied zwischen einem moderaten und einem deutlichen Speedup ausmachen. Tabelle 4 stellt die drei praktikablen Trainingsvarianten mit ihrem jeweiligen Aufwand-Nutzen-Verhältnis gegenüber.

Strategie Aufwand Codeabdeckung Profiling-Informationen
Exit bei Context Refresh niedrig Fokus auf Startpfad und Klassenladung begrenzt
Smoke- oder Integrationsprofil mittel breitere Hot Paths gut
Produktionsnahe Lastsequenz hoch Hot Paths plus Randfälle sehr gut

Tabelle 4: Drei praktikable Trainingsstrategien

Um Trainingsläufe stabil und reproduzierbar zu halten, sind kontrollierte Testbedingungen entscheidend. Lokale oder containerisierte Datenquellen ersetzen produktive Systeme, durch mockbare Integrationen werden Trainings frei von externen Abhängigkeiten gehalten und kurze Lastsequenzen sorgen dafür, dass der Build-Schritt nicht zum Engpass wird. Eine produktive Datenbank, eine echte Message-Queue oder ein Authentication-Provider haben im Trainingslauf nichts zu suchen – nicht zuletzt aus Sicherheitsgründen. Dazu später mehr.

Zur Diagnose lohnt sich der Einsatz des Java Flight Recorders. Mit seiner Hilfe wird sichtbar, welche Klassen tatsächlich geladen wurden und welche Methoden während des Trainings heiß waren. Das verhindert einen Blindflug bei der Optimierung und macht regressive Veränderungen früh sichtbar, beispielsweise wenn ein eingeführtes Test-Framework plötzlich Tausende zusätzlicher Klassen in den Cache zieht. Listing 7 zeigt die drei Schritte vom JFR-Setup bis zur Auswertung.

Listing 7: Trainingslauf mit Java Flight Recorder beobachten

# 1) Klassenlade-Events aktivieren
jfr configure --output custom.jfc jdk.ClassLoad#enabled=true

# 2) Training mit Recording durchführen
java -XX:StartFlightRecording:settings=custom.jfc,duration=60s,filename=/tmp/AOT.jfr \
     -cp app.jar com.example.App

# 3) Relevante Events auswerten
jfr print --events "jdk.ClassLoad" /tmp/AOT.jfr
jfr view hot-methods /tmp/AOT.jfr

Ein einfacher Smoke-Harness reicht oft aus, um realistischere Trainingspfade zu erreichen. Wichtig ist nicht die Menge der Aufrufe, sondern ihre Relevanz für den produktiven Einstiegspfad. Erfahrungsgemäß decken drei Curl-Aufrufe gegen die Health-, die meistgenutzte Lese- und die wichtigste Schreibroute bereits einen sehr großen Teil des Hot Paths ab. Listing 8 skizziert ein solches Minimal-Setup.

Listing 8: Minimaler Smoke-Harness für ein repräsentatives Training

#!/usr/bin/env bash
set -euo pipefail

curl -sf http://localhost:8080/actuator/health   >/dev/null
curl -sf http://localhost:8080/api/orders        >/dev/null
curl -sf http://localhost:8080/api/customers     >/dev/null

In Service-Landschaften mit vielen Komponenten ist es sinnvoll, pro Service ein eigenes Trainingsprofil zu pflegen, statt ein universelles Skript zu erzwingen. So bleiben die Caches klein, reproduzierbar und fachlich nachvollziehbar. Der Wartungsaufwand pro Service ist gering, der Effekt auf Cache-Größe und Speed-up oft beträchtlich.

Anforderungen, Konsistenz und Cache-Invalidierung

AOT-Caches sind bewusst eng an ihre technische Umgebung gebunden. Das ist jedoch keine Schwäche, sondern die Voraussetzung für belastbare Ergebnisse. Wer diese Regeln ignoriert, bekommt im besten Fall keinen Speed-up, im schlechteren Fall einen stillschweigend deaktivierten Cache, der erst beim genauen Hinsehen auffällt. Tabelle 5 fasst die notwendigen Bedingungen zusammen.

Bereich Notwendige Bedingung
JDK identische oder kompatibel freigegebene Version zwischen Training und Runtime
Plattform gleiche Hardware-Architektur (möglichst identisches OS-Image)
Classpath konsistente JAR-Struktur; Runtime darf eine Obermenge des Trainings sein
Moduloptionen –add-opens, –add-exports, –add-reads, –patch-module, –upgrade-module-path dürfen nicht verwendet werden
Artefaktstruktur stabile Inhalte und reproduzierbare JAR-Timestamps

Tabelle 5: Anforderungen an einen belastbaren AOT-Cache

Aus diesen Anforderungen ergibt sich unmittelbar eine Liste der Ereignisse, bei denen ein Cache invalidiert werden muss. Jede Codeänderung, jedes Dependency-Update, jedes JDK-Upgrade, jede Veränderung der Build-Artefaktstruktur erzwingt einen Rebuild. In der Praxis ist das weniger dramatisch, als es klingt, da dieser Trainingsschritt ohnehin in die CI/CD-Pipeline gehört und pro Build automatisch ausgeführt wird. Eine Cache-Wiederverwendung über mehrere Versionen hinweg sollte explizit vermieden werden, auch wenn sie technisch möglich erscheint. Der eingesparte Build-Schritt ist den Diagnoseaufwand bei Inkonsistenzen nicht wert.

Ein in diesem Zusammenhang bislang oft unterbelichteter Aspekt ist die Sicherheit. Trainingsläufe haben in produktiven Umgebungen nichts verloren und sie dürfen keine produktiven Geheimnisse berühren. Dazu zählen Datenbank-Passwörter, API-Schlüssel, OAuth-Client-Secrets und Tokens. Trainings sollten ausschließlich gegen Dummy-Konfigurationen, lokale H2-Instanzen oder Testcontainer laufen. Die Policy lautet: Keine produktiven Secrets in Build-Time-Ausführungen! CI/CD-Pipelines sollten dies durch Secrets-Scanner und klar getrennte Berechtigungsräume aktiv durchsetzen.

Hinsichtlich der Garbage Collection gilt eine wichtige Übergangsregel. In JDK 24 und JDK 25 werden gecachte Objekte in einem GC-spezifischen Format gespeichert, das mit Serial, Parallel und G1 kompatibel ist, nicht jedoch mit ZGC. Wer also bisher ZGC einsetzt, muss sich zwischen niedrigen GC-Latenzen und AOT-Cache entscheiden. Mit JEP 516 in JDK 26 entfällt diese Einschränkung. Ein zusätzliches, GC-neutrales Streaming-Format macht AOT-Object-Caching auch mit ZGC möglich, was insbesondere für Anwendungen mit großem Heap und niedrigen Latenzanforderungen relevant ist [5].

Container und CI/CD: vom reproduzierbaren Build zum skalierbaren Deployment

Der größte operative Vorteil entsteht, wenn Cache-Erzeugung und -Nutzung in derselben kontrollierten Pipeline erfolgen. Für Teams bieten sich dabei zwei stabile Wege an: Spring Boot Buildpacks für Standardisierung oder ein mehrstufiges Dockerfile für maximale Transparenz. Beide Wege haben ihre Berechtigung und schließen einander nicht aus.

Paketo-Buildpacks für standardisierte Pipelines

Aktuelle Versionen der Paketo-Java-Buildpacks unterstützen neben CDS auch den AOT-Cache. Die Aktivierung erfolgt über Build-Properties, der Trainingslauf läuft im Build-Container automatisch. Das hat den charmanten Effekt, dass dasselbe JVM-Image für Training und Deployment verwendet wird, sodass die Konsistenzanforderungen aus Tabelle 5 damit per Konstruktion erfüllt sind. Die konkreten Variablennamen und die unterstützten JVM-Versionen ändern sich mit den Releases der Java- und JVM-Buildpacks. Die jeweils aktuellen Werte sollten direkt der Paketo-Dokumentation entnommen werden [9]. Listing 9 zeigt das Schema einer typischen Konfiguration im Spring-Boot-Maven-Plugin.

Listing 9: AOT-Cache im Maven-Buildpack-Setup

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <image>
      <env>
        <BP_JVM_VERSION>25</BP_JVM_VERSION>
        <BP_JVM_AOTCACHE_ENABLED>true</BP_JVM_AOTCACHE_ENABLED>
        <BP_SPRING_AOT_ENABLED>true</BP_SPRING_AOT_ENABLED>
      </env>
    </image>
  </configuration>
</plugin>

Wer mehr Kontrolle wünscht, beispielsweise um den Trainingslauf gegen profilingrelevante Endpunkte auszuführen statt nur Exit-on-Refresh zu nutzen, greift weiterhin zum eigenen Dockerfile. Ein mehrstufiges Setup trennt Build, Training und Runtime sauber voneinander und macht so jede Phase einzeln reproduzierbar. Dabei ist eine wichtige Regel unübersehbar: Die Training- und Runtime-Stage müssen exakt dasselbe JVM-Basis-Image verwenden, da die JVM sonst zur Laufzeit den Cache ablehnt. Listing 10 zeigt eine kompakte Variante mit Eclipse Temurin als Basis.

Mehrstufiges Dockerfile mit Trainings-Stage

Listing 10: Mehrstufiges Dockerfile mit Build, Training und Runtime

FROM eclipse-temurin:25-jdk AS builder
WORKDIR /app
COPY . .
RUN ./mvnw -q -DskipTests package \
    && java -Djarmode=tools -jar target/aot-cache-demo.jar \
            extract --destination extracted

FROM eclipse-temurin:25-jre AS trainer
WORKDIR /app
COPY --from=builder /app/extracted/ ./
RUN java -XX:AOTCacheOutput=app.aot \
         -Dspring.context.exit=onRefresh \
         -jar aot-cache-demo.jar

FROM eclipse-temurin:25-jre
WORKDIR /app
COPY --from=builder /app/extracted/ ./
COPY --from=trainer /app/app.aot ./
ENTRYPOINT ["java", "-XX:AOTCache=app.aot", "-jar", "aot-cache-demo.jar"]

In Kubernetes-Umgebungen empfiehlt es sich, den Cache direkt in das Container-Image einzubetten. So werden Volumes vermieden, deren Lebenszyklus von dem der Anwendung entkoppelt ist, und es wird sichergestellt, dass jede Image-Version ihren passenden Cache mitbringt. Werden Caches hingegen separat in einem Volume gehalten, muss bei Rollouts sehr genau über Versionierung und Konsistenz nachgedacht werden. Die einfachere und in den meisten Fällen bessere Lösung ist und bleibt: ein Cache pro Image, ein Image pro Version.

In CI/CD-Pipelines ist die Cache-Erzeugung als verpflichtender Build-Step zu führen. Cache-Artefakte sollten pro Architektur und JDK eindeutig getaggt werden – ein Cache für x64 funktioniert nicht auf aarch64 und umgekehrt. Bei jeder neuen Anwendungsversion entsteht ein neuer Cache. Ein Reuse auf Verdacht spart zwar wenige Sekunden Build-Zeit, kostet aber im Zweifelsfall aber Stunden für die Diagnose. Wer zusätzlich reproduzierbare JAR-Timestamps erzwingt – in der Maven-Welt am einfachsten über die Property project.build.outputTimestamp in der Datei pom.xml, die durch mvn artifact:check-buildplan geprüft wird –, schafft die Voraussetzung für stabile Caches über CI-Worker hinweg.

AOT-Cache im Vergleich zu CRaC und GraalVM

Es gibt nicht den einen richtigen Weg zu einem schnellen Java-Start-up. AOT-Cache, CRaC [11] und GraalVM Native Image [12] sind drei unterschiedliche Antworten auf dieselbe Frage, mit jeweils eigenen Stärken und Trade-offs. Die Auswahl sollte sich nach dem konkreten Workload, der Plattform und der organisatorischen Reife des Teams richten und nicht nach Trends.

CRaC (Coordinated Restore at Checkpoint) geht dabei den radikalsten Weg. Ein laufender Prozess wird per CRIU eingefroren und später wiederhergestellt. Der Restore ist extrem schnell, da Klassen, Caches und Heap bereits im stationären Zustand wiederhergestellt werden. Der Preis dafür liegt jedoch in der höheren Betriebskomplexität. CRaC erfordert Linux mit CRIU, eine bewusste Behandlung offener Datei-, Netzwerk- und Datenbank-Handles vor dem Checkpoint sowie einen Sicherheitsreview der Snapshot-Inhalte. Secrets, die zum Checkpoint-Zeitpunkt im Speicher liegen, landen ungewollt im Snapshot und müssen daher aktiv ausgeschlossen werden. Für Teams mit harten Start-up-Zielen und passender Plattform ist CRaC eine starke Option, für viele Standard-Workloads bleibt der Aufwand jedoch hoch.

GraalVM Native Image kompiliert eine Anwendung dagegen statisch zu einem nativen Binary. Das Resultat sind ein Start-up im Bereich von Millisekunden und ein deutlich reduzierter Footprint. Der Preis dafür ist die Closed-World-Assumption: Reflection, dynamische Proxies und Resource Loading müssen vorab konfiguriert werden, die Build-Zeiten sind länger und manche Bibliotheken erfordern explizite Kompatibilitätsarbeit. Für Serverless Functions, CLI-Tools und Microservices mit extremen Start-up-Anforderungen ist Native Image häufig die richtige Wahl. Für klassische Webanwendungen und APIs auf langlebigen Instanzen ist die JVM mit AOT-Cache meist flexibler und besser wartbar.

In dieser Landschaft positioniert sich der AOT-Cache als pragmatischer Mittelweg. Er erfordert keine neue Plattform, keine Closed-World-Konfiguration und keinen Snapshot-Mechanismus. Stattdessen bringt er einen klaren, reproduzierbaren Workflow mit, der sich gut in bestehende CI/CD-Strukturen einfügt. Für die meisten Enterprise-Teams stellt er den besten Kompromiss aus Wartbarkeit, Performance und Tooling dar. CRaC und Native Image bleiben auch weiterhin sinnvolle Spezialwerkzeuge für klar abgegrenzte Szenarien.

Wann lohnt sich AOT-Caching nicht?

Nicht alle Workloads profitieren von AOT-Caching. Kurzlebige CLI-Tools und Batch-Jobs, die nach wenigen Sekunden ohnehin beendet sind, profitieren kaum. Der Trainingslauf in der Pipeline kostet mehr Zeit, als der Cache zur Laufzeit jemals einspart. Ähnlich verhält es sich mit klassischen, langlebigen Monolithen, die nur einmal pro Quartal deployt werden. Hier ist CDS oft völlig ausreichend. Echte Serverless-Funktionen mit Sub-Sekunden-Anforderungen wiederum liegen jenseits dessen, was die JVM realistisch leisten kann. Hierfür bleibt GraalVM Native Image das passendere Werkzeug. Anwendungen, die zur Laufzeit massiv Klassen generieren (etwa über Bytecode-Manipulation auf jedem Request), erhalten durch den AOT-Cache zwar einen schnelleren Start, aber keinen Warm-up-Vorteil im späteren Lebenszyklus. Die Regel lautet: Ein AOT-Cache lohnt sich, wenn der Start-up-Pfad reproduzierbar und im Verhältnis zur Lebensdauer einer Instanz relevant ist.

Troubleshooting und Praxistipps

Die meisten Probleme rund um den AOT-Cache sind diagnostizierbar, sobald Logging, Metriken und Vergleichsläufe standardisiert sind. Wer mit den richtigen Werkzeugen systematisch vorgeht, findet in der Regel innerhalb weniger Minuten die Ursache. Tabelle 6 fasst die häufigsten Symptome und ihre wahrscheinliche Ursache zusammen.

Symptom Wahrscheinliche Ursache Reaktion
AOT cache cannot be used Classpath-Mismatch Training- und Runtime-Classpath angleichen
Cache lädt, aber kein Speedup Training nicht repräsentativ kritische Endpunkte in Training aufnehmen
Cache-Datei sehr groß Trainingsumfang zu breit Mocks nutzen, Test-Frameworks ausschließen
ClassNotFoundException zur Laufzeit Runtime-Classpath fehlt JARs Classpath als Obermenge sicherstellen
Stiller Start ohne Cache Cache-Datei nicht gefunden oder inkompatibel strikten Modus aktivieren, Logs auswerten

Tabelle 6: Typische Fehlerbilder und Erstreaktionen

Für die laufende Optimierung lohnen sich standardisierte Vergleichsmessungen. Ein einfacher Zeitvergleich zwischen einem Lauf ohne und einem Lauf mit Cache liefert eine erste belastbare Zahl. Eine zusätzliche JFR-Aufzeichnung der ersten 30 Sekunden zeigt, wo Hot Methods und GC-Aktivität liegen. So wird sichtbar, ob ein Cache nur die Klassenladung beschleunigt oder auch das Warm-up verkürzt. Listing 11 zeigt das passende Kommandoset dafür.

Listing 11: Vergleichsmessung ohne und mit Cache

time java -jar app.jar > /dev/null
time java -XX:AOTCache=app.aot -jar app.jar > /dev/null

java -XX:AOTCache=app.aot \
     -XX:StartFlightRecording:settings=default,duration=30s,filename=profile.jfr \
     -jar app.jar

jfr view hot-methods profile.jfr
jfr view gc          profile.jfr

In der Praxis tauchen drei Stolpersteine besonders häufig auf. Erstens gibt es oft unterschiedliche JAR-Timestamps zwischen Training und Deployment, was häufig durch nicht reproduzierbare Builds ausgelöst wird. Zweitens: ein Deployment-Classpath, der weniger JARs enthält als das Training. Die Anforderung lautet hier, dass der Runtime-Classpath eine Obermenge sein muss. Und drittens: unbemerkt unterschiedliche JDK-Versionen oder Architekturen – etwa, wenn ein lokaler Build auf aarch64 entstand und auf x64-Worker deployt wird. Alle drei Punkte sollten in einer Pre-Deployment-Checkliste berücksichtigt werden, die als Gate vor dem Rollout fungiert.

Für das Cache-Tuning auf Anwendungsseite gelten drei einfache Regeln. Erstens ist der Cache schlank zu halten, indem Test- und Hilfsframeworks aus dem Training ferngehalten werden. Zweitens sollten reproduzierbare Build-Timestamps erzwungen werden, damit Cache-Inhalte über CI-Worker hinweg deterministisch werden. Und drittens müssen separate Caches für x64 und aarch64 gepflegt werden, statt einen universellen Cache zu erzwingen, den die JVM ohnehin verweigern würde.

Pre-Deployment-Checkliste für den AOT-Cache

  • JDK-Version und Vendor zwischen Training und Runtime identisch

  • CPU-Architektur (x64 / aarch64) zwischen Build- und Ziel-Stage identisch

  • JAR-Inhalte und -Timestamps zwischen Training und Deployment unverändert (project.build.outputTimestamp)

  • Runtime-Classpath ist Obermenge des Trainings-Classpaths, nur JAR-Dateien, keine Verzeichnisse

  • Trainingslauf ohne produktive Secrets, gegen Mocks oder Testcontainer

  • Strikter Modus in CI/Staging aktiviert, in Produktion stattdessen Logging und Metriken

  • Vergleichsmessung tready ohne und mit Cache dokumentiert

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Fazit

AOT-Caching ist im Jahr 2026 ein ausgereiftes Werkzeug. Mit JDK 24 bis 26 steht ein technisch belastbarer Pfad bereit, der die Stärken der JVM erhält und gleichzeitig Start-up- und Warm-up-Zeiten in cloud-nativen Betriebsmodellen spürbar reduziert. Im Unterschied zu Native Image bleibt die volle Java-Plattform-Kompatibilität erhalten und anders als bei CRaC fällt keine zusätzliche Linux- und Snapshot-Komplexität an. Damit ist AOT-Caching für viele Teams der pragmatischste nächste Schritt nach einer CDS-Baseline.

Ein nachhaltiger technischer Gewinn entsteht jedoch nur dann, wenn der Prozess diszipliniert umgesetzt wird. Die zentralen Punkte sind eindeutig: konsistente Artefaktstruktur, reproduzierbares Training, saubere Verifikation, verbindliche Pipeline-Schritte und ein bewusster Umgang mit Secrets. Wer diese Regeln einhält, gewinnt nicht nur an Geschwindigkeit beim Start, sondern auch an Vorhersagbarkeit im Betrieb. Kombinationen mit Spring AOT und reproduzierbaren Buildpacks verstärken diesen Effekt zusätzlich, ohne die zugrunde liegende Mechanik zu verändern.

Der praktikable Einstieg lässt sich in fünf Schritten zusammenfassen: Baseline messen, Extracted Layout etablieren, ersten Cache mit Exit-on-Refresh erzeugen, mit Logs und JFR validieren, Trainingsqualität schrittweise verbessern. Mit jedem JDK-Release wird das Verfahren leistungsfähiger. Method Profiling in JDK 25 und Object Caching mit beliebigem GC in JDK 26 sind dabei nur zwei Bausteine einer längeren Roadmap. Wer heute beginnt, profitiert automatisch mit jedem Upgrade. Genau das macht AOT-Caching zu einer Investition mit überschaubarem Aufwand und langem Wirkzeitraum.

The post Project Leyden: AOT-Caching für Java & Spring Boot appeared first on JAX.

]]>
Taktisches DDD für wartbaren KI-generierten Code https://jax.de/blog/taktisches-ddd-ki-generierter-code/ Thu, 23 Jul 2026 13:56:41 +0000 https://jax.de/?p=210668 Copiloten generieren in Sekunden lauffähigen Code, doch wer übernimmt die Verantwortung für fachliche Integrität, Wartbarkeit und Architektur? Dieser Artikel zeigt, warum taktisches Domain-Driven Design im KI-Zeitalter zum unverzichtbaren Ordnungsprinzip wird und wie es als Leitplanke für generierten Code dient.

The post Taktisches DDD für wartbaren KI-generierten Code appeared first on JAX.

]]>

Hinweis: Dieser Podcast und dieses Video wurden mithilfe von KI erstellt. Dabei wurden die Originalinhalte und technischen Erkenntnisse des Autors des Blogbeitrags adaptiert.

 

Der aktuelle State of the Art im Software Engineering ist untrennbar mit Künstlicher Intelligenz verbunden. Wo früher erhebliche Zeit in das Durchforsten von Bibliotheks-Dokumentationen, das Schreiben von repetitivem Boilerplate-Code oder das mühsame Aufsetzen technischer Grundgerüste floss, liefern Copiloten heute in Sekundenschnelle erste Lösungsvorschläge. Das reine Beherrschen einer Programmiersprache, also das Wissen um die exakte Grammatik und die verfügbaren Standard-APIs, verliert deutlich an Bedeutung, da die Maschine diese Zusammenhänge auf Abruf bereitstellt. Der Begriff Copilot steht in dieser Serie stellvertretend für KI-gestützte Coding-Agents, sei es GitHub Copilot, JetBrains Junie, Cursor, Claude Code oder ein anderes Werkzeug. Die Argumentation gilt für alle gleichermaßen, weil sie auf der gemeinsamen Funktionsweise und nicht auf einem konkreten Produkt aufbaut.

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Vom Coder zum Architekten und Prüfer

Mit diesem Wandel verschiebt sich der Fokus. Die präzise Spezifikation rückt ins Zentrum der Arbeit. Es keimt die verlockende Vision auf, direkt von einer fachlichen Beschreibung zur fertigen technischen Lösung zu springen. Doch Vorsicht ist geboten: Die KI glänzt zwar durch syntaktisch korrekten und lauffähigen Code, nutzt diese Fähigkeit aber oft, um Lücken in einer ungenauen Spezifikation „kreativ” (und potenziell fachlich falsch) zu schließen. Der Mensch verlässt zunehmend die Rolle des Schreibenden und wird zum Architekten und Prüfer. Um diese Rolle auszufüllen, ist ein tieferes Verständnis der fachlichen Konzepte und architektonischen bzw. Softwaredesignvorgaben notwendiger denn je.

Agilität in Zeiten der Generierung

Trotz der Geschwindigkeit der KI bleibt Agilität der entscheidende Erfolgsfaktor. Warum?

  • Perfekte Anforderungen sind eine Illusion: Es war noch nie möglich, zu Projektbeginn perfekte Anforderungen zu erheben.
  • Lernen durch Tun: Der eigentliche Erkenntnisgewinn über fachliche Zusammenhänge entsteht erst während des Entwicklungsprozesses. Und Agilität ist die Disziplin, die daraus entstehendes Wissen kontinuierlich in das Projekt zurückführt.
  • Permanenter Wandel: Anforderungen und Rahmenbedingungen ändern sich heute schneller als je zuvor. Change kommt verstärkt von außen, die Welt dreht sich weiter.

Wir müssen Systeme also ständig anpassen und überprüfen. Genau hier wird Softwaredesign zur Voraussetzung von Agilität: Erst anpassbare und erweiterbare Strukturen machen das ständige Reagieren auf neue Erkenntnisse und veränderte Rahmenbedingungen wirtschaftlich tragfähig. Das gilt auf allen Ebenen der Architektur, im Rahmen dieser Artikelserie aber besonders für das Softwaredesign der Geschäftsdomäne.

Warum Tests und reine Fachprüfung allein nicht ausreichen

Es wäre jedoch zu kurz gegriffen, die neue Rolle des Menschen allein auf das Abgleichen von fachlichen Anforderungen mit den KI-Ergebnissen zu reduzieren. Ein funktionierendes System ist mehr als die Summe seiner generierten Funktionen. Es benötigt eine innere Ordnung. Wer sich blind auf die KI verlässt, erhält zwar lauffähige Bausteine, verliert aber das Systemverständnis für deren Zusammenspiel. In dieser Lücke zwischen „Code, der irgendwie funktioniert” und „nachhaltigem Softwaredesign” offenbart sich das Risiko der KI-gestützten Entwicklung.

Oft wird die Testabdeckung als Rettungsanker ins Feld geführt. Doch hier ist Vorsicht geboten: Eine 100%ige Testabdeckung ist in der Praxis eine Illusion und oft gar nicht sinnvoll. Vor allem aber heilt sie kein schlechtes Design. Wenn ein Test fehlschlägt, ist das lediglich ein Symptom. Ohne ein tiefes Verständnis des zugrunde liegenden Softwaredesigns lässt sich kaum beurteilen, ob wir lediglich ein technisches Detail korrigieren müssen oder ob das gesamte Konzept in sich widersprüchlich ist und angepasst werden muss. Ein Designdefizit führt zu handfesten Risiken: eine strukturelle Erosion des Codes, der Verlust des mentalen Modells beim Entwickler und eine schleichende logische Inkonsistenz, die durch oberflächliche Tests nicht abgefangen wird.

DIE KUNST DER SOTWARE-ARCHITEKTUR

Architecture & Design-Track entdecken

 

Taktisches DDD als Ordnungsprinzip

Hier tritt das taktische Domain-Driven Design (taktisches DDD) auf den Plan. Während das strategische DDD den Blick auf die großen fachlichen Zusammenhänge richtet, bietet das taktische DDD konkrete Entwurfsmuster für die Implementierung innerhalb einer Domäne.

Kurz gefasst ist taktisches DDD ein formaler Werkzeugkasten, um fachliche Regeln (Invarianten) direkt in die Struktur des Codes zu gießen. Statt Logik willkürlich über das System zu verstreuen, nutzt es spezifische Bausteine:

  • Entities: Objekte mit einer eindeutigen Identität, die einen Lebenszyklus durchlaufen (z. B. ein „Kunde”).
  • Value Objects: Unveränderliche Objekte, die durch ihre Werte definiert sind (z. B. eine „IBAN” oder ein „GeldBetrag”). Sie sind der Schlüssel zur Vermeidung technischer Primitive.
  • Aggregates: Eine Gruppe von Objekten, die als Einheit betrachtet werden. Das sogenannte Aggregate Root garantiert als einzige Zugriffsschnittstelle, dass alle fachlichen Regeln innerhalb dieser Gruppe jederzeit eingehalten werden.
  • Domain Services: Ort für Fachlogik, die nicht sinnvoll in eine einzelne Entity oder ein Value Object passt.

Diese Bausteine sorgen für Strukturen, die das Verständnis und die Wartbarkeit spürbar verbessern. Eine detaillierte Analyse dieser Bausteine und weiterer Bausteine sowie deren Implementierung im Zusammenspiel mit KI folgt in den Folgeartikeln dieser Serie anhand eines konkreten Praxisbeispiels.

Bevor wir dann die Vorteile von taktischem DDD im Rahmen KI-gestützter Softwareentwicklung etwas genauer betrachten, noch ein Blick auf einen verwandten, aktuell stark diskutierten Aspekt.

Der „Battle of Truth”: Domänenmodell vs. Spezifikation

In der aktuellen Architektur-Community tobt ein spannender Streit: Was ist die eigentliche Single Source of Truth? Die Fronten unterteilen sich dabei wie folgt:

  • Die Spec-Sovereignty-Fraktion: Sie argumentiert, dass die Spezifikation (z. B. in Form von strukturierten Markdown-Modellen, Mermaid-Diagrammen oder Agent-Blueprints) die Wahrheit ist. Der Java-Code ist (bald) nur noch ein „Artefakt”, das von einer KI aus der Spec generiert wird.
  • Die Code-Sovereignty-Fraktion (Klassisches DDD): Sie beharrt darauf, dass nur der Code die Wahrheit ist, da nur er die tatsächliche Laufzeit-Realität abbildet. Eine Spec kann lügen (ggf. einfach nur veraltet sein); Code tut es nicht.

Warum die Code-Souveränität das stabilere Fundament bleibt

Es spricht vieles dafür, dass das Primat des Codes bestehen bleibt – nicht aus Technikverliebtheit, sondern aus drei fundamentalen Gründen:

  • Realität zur Laufzeit: Der Code ist das einzige Artefakt, das tatsächlich ausgeführt wird. Alles davor ist Intention, alles danach Beobachtung. Eine Spezifikation beschreibt, was sein soll; der Code beschreibt, was ist.
  • Deterministische Verarbeitung: Ein Compiler liefert bei gleicher Eingabe immer dasselbe Ergebnis. Generative Modelle tun das nicht, nicht einmal annähernd. Diese Lücke zwischen Spec und Laufzeit muss durch den Code selbst geschlossen werden, durch die Art, wie er strukturiert ist. Taktisches DDD verfolgt dabei eine klare Grundhaltung: Die Fachlichkeit soll möglichst explizit im Code abgebildet werden, nicht zwischen den Zeilen mitgedacht oder in Konventionen verborgen. Value Objects und Aggregates sind die konsequente Umsetzung dieser Haltung: Sie kapseln Invarianten so, dass die fachliche Wahrheit nicht in einem variablen LLM-Output liegt, sondern im typisierten, kompilierten Code.
  • Präzision in begrenzten Kontexten: Eine textuelle Spezifikation kann Mehrdeutigkeiten enthalten, ohne dass es jemandem auffällt. Ein Typsystem kann das nicht, Money und BigDecimal sind verschiedene Dinge, und der Compiler zwingt zur Entscheidung. Diese Präzisionstiefe lässt sich allerdings nur dort erreichen, wo der Kontext überschaubar bleibt. Genau das leisten DDD-Bausteine: Ein Aggregate definiert eine klar abgegrenzte Verantwortungsgrenze, innerhalb derer sowohl Mensch als auch KI mit hoher Genauigkeit arbeiten können. Ohne solche Grenzen verliert sich jede Implementierung, egal ob von Hand geschrieben oder generiert, in der Komplexität des Gesamtsystems. Im Sinne des taktischen DDD ist der Code die exakteste und detailreichste Abbildung des Fachmodells. Hier werden Invarianten und Business-Regeln, Strukturen und Abläufe in einer Tiefe definiert, die eine rein textuelle Spezifikation kaum erreichen kann.

Die empirischen Belege für diese Grenzen aktueller LLMs, Nicht-Determinismus, Reasoning Failures, Kontext-Erosion, fassen wir im Kasten „Wissenschaftliche Evidenz: Warum die KI klare Strukturen braucht” zusammen.

Wissenschaftliche Evidenz: Warum die KI klare Strukturen braucht

Die im Haupttext genannten LLM-Schwächen sind empirisch belegt. Drei aktuelle Befunde sind für die DDD-Argumentation besonders relevant:

  • Nichtdeterminismus trotz Temperature 0 [2]: Selbst bei Temperature 0, also der Einstellung, die theoretisch Determinismus garantiert, variieren Cloud-LLM-Ausgaben signifikant. Implikation: Reproduzierbarkeit muss durch deterministische Abbildung der Fachlichkeit im Code erzwungen werden, unter anderem durch Typen, Tests, Guard-Clauses und Aggregate-Grenzen, nicht durch Prompt-Tuning.
  • Lost-in-the-Middle bei langen Kontexten [3]: Informationen in der Mitte langer Prompts werden de-priorisiert. Implikation: Kontextfenster klein halten. Also exakt das, was die Kapselung in DDD-Bausteinen erreicht. Aggregate-Grenzen sind dafür das prominenteste Beispiel; Value Objects, Domain Services und andere Bausteine wirken auf derselben Ebene.
  • Reasoning-Failures bei mehrstufiger Logik [4]: LLMs operieren statistisch, nicht deduktiv. Reasoning-Modelle und Agent-Ketten haben das Risiko von Schlussfolgerungs-Fehlern deutlich reduziert, eliminiert haben sie es nicht. Implikation: Der Prompt ist die Anweisung, der Code ist die Wahrheit. Geschäftsregeln können und sollen in Prompts vorkommen. Verbindlich werden sie aber erst dort, wo Aggregates und Assertions sie maschinell erzwingen. Mit jeder Erweiterung verschiebt sich das Gesamtzusammenspiel, und nur die im Code geprüften Regeln sichern die Konsistenz über die Zeit.

Engineering statt Illusion: Das Zusammenspiel der Wahrheiten

Dabei darf man sich keinen Illusionen hingeben: Weder eine perfekte Spezifikation noch eine lückenlose Testabdeckung sind in der Praxis wirklich erreichbar. Dennoch bleiben beide Säulen für qualitativ hochwertiges Software-Engineering unverzichtbar.

Eine saubere, strukturierte Spezifikation ist nach wie vor das Fundament jeder professionellen Entwicklung, sie ist die Quelle der Absicht. Doch so präzise sie auch sein mag, es bleibt aus den genannten Gründen (Nicht-Determinismus der KI, logische Unschärfen, Schwierigkeiten beim Umgang mit großen Kontexten) schwierig, die Spezifikation als die einzige Wahrheit anzusehen. Ebenso verhält es sich mit Tests: Eine 100%ige Abdeckung ist oft eine ökonomische und theoretische Illusion. Dennoch sind Unit-Tests kein optionaler Luxus, sondern essenzielles Engineering. Sie bilden die pragmatische Brücke, um die Kluft zwischen der Spezifikation und der tatsächlichen Ausführung so gering wie möglich zu halten.

Wenn wir akzeptieren, dass weder die Spec unfehlbar noch der Test lückenlos ist, rückt ein dritter Faktor ins Zentrum: Aufbau und Struktur des Codes. Nur weil wir nicht mehr jede Zeile von Hand schreiben, verliert Softwaredesign nicht an Bedeutung, im Gegenteil. Der strukturelle Aufbau beeinflusst maßgeblich die entscheidenden Qualitätsparameter, wie Änderbarkeit und Erweiterbarkeit.

Es ist ein gefährlicher Trugschluss zu glauben, dass Wartbarkeit im Zeitalter der KI „kostenlos” oder vernachlässigbar sei. Ein schlecht strukturierter „Big Ball of Mud” lässt sich auch mit KI-Unterstützung nur unter hohem Risiko und hohem Aufwand anpassen. Die kognitive Last beim Refactoring und die Gefahr von Seiteneffekten steigen ohne sauberes Design überproportional an, unabhängig davon, wer oder was die Tastatur bedient.

Da eine lückenlose formale Verifikation meist am Aufwand scheitert oder an theoretische Grenzen stößt, bleibt der Code die einzige finale Instanz der Wahrheit. Er ist die detailreichste Abbildung der Fachlichkeit. Aber nur dann, wenn die Codebasis selbst aufgeräumt und nachvollziehbar strukturiert ist. Ohne diese strukturelle Qualität funktioniert weder klassische Wartung noch KI-gestützte Weiterentwicklung. Für die Geschäftslogik fachlich getriebener Domänen liefert taktisches DDD diese Qualität auf besonders wirksame Weise: Es gibt dem Code eine Struktur, die ihn kontrollierbar, prüfbar und vor allem langfristig änderbar macht. In der Welt des DDD bleibt der Code damit das „Living Document”, das die Realität der Domäne widerspruchsfrei abbildet.

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Reasoning-Modelle und Agenten: Was sich ändert, was bleibt

Man könnte einwenden: Die hier beschriebenen LLM-Schwächen seien Stand 2024, längst eingeholt durch Reasoning-Modelle und agentische Systeme. Tatsächlich hat sich vieles bewegt. Modelle mit explizitem Reasoning-Schritt zeigen in formalen Logikaufgaben, mathematischen Beweisen und mehrstufigen Schlussfolgerungen deutlich bessere Ergebnisse als die Generation davor. Agentische Systeme können Code ausführen, Tests laufen lassen, Fehler beobachten und ihren Ansatz korrigieren. Sie sind nicht mehr auf einen einzigen Generierungsschritt ohne Rückkopplung angewiesen. Das verändert die DDD-Argumentation an einer Stelle und stärkt sie an einer anderen.

Verändert hat sich der Punkt der reinen Reasoning-Defizite. Ein modernes Reasoning-Modell wird einen Aggregate-Konsistenz-Check, der sich aus drei verschachtelten Invarianten ergibt, in vielen Fällen korrekt durchdenken. Das war 2024 noch eine zuverlässige Fehlerquelle, 2026 nicht mehr durchgängig. Auch die Halluzination einzelner API-Aufrufe ist seltener geworden, weil agentische Systeme ihre Annahmen gegen die echte Codebasis prüfen können.

Gestärkt hat sich dagegen das Kontextargument, und das ist die wichtigere Beobachtung. Agentische Systeme arbeiten typischerweise mit deutlich größeren Kontextfenstern und über längere Zeiträume hinweg. Sie lesen Dateien, planen mehrstufige Änderungen, schreiben über Modulgrenzen hinweg. Dass moderne Kontextfenster eine Million Tokens umfassen, ändert daran weniger als erwartet: Was zählt, ist nicht die theoretische Größe, sondern der effektive Aufmerksamkeitsspielraum, in dem verlässlich verschachtelt geschlussfolgert werden kann. Genau in diesem Modus wird die Strukturqualität der Codebasis zum Engpass. Ein Agent, der eine unstrukturierte Codebasis analysieren soll, verzweigt sich in Abhängigkeiten, bis sein effektiver Kontext kollabiert. In einer sauber geschnittenen Aggregate-Landschaft kann er dagegen präzise innerhalb einer Verantwortungsgrenze arbeiten. Je autonomer die KI wird, desto teurer wird schlechtes Design, nicht billiger.

Hinzu kommt ein zweiter Effekt: Reasoning-Modelle und Agenten sind teuer. Pro Aufruf, pro Token, pro Werkzeugzyklus. Wer ihnen einen sauber strukturierten Kontext liefert, bekommt das Ergebnis in einem Bruchteil der Schritte. Taktisches DDD wird damit nicht nur zur Qualitäts-, sondern auch zur Kosten-Frage.

Die Pointe bleibt also dieselbe, aber sie verschiebt sich. War taktisches DDD 2024 vor allem ein Korrektiv für schwache Generierung, ist es 2026 ein Verstärker für starke Autonomie. Eine gute KI in einem schlechten Design produziert schneller schlechte Software. Eine gute KI in einem DDD-konformen Design produziert schneller gute Software. Der Hebel ist immer die Struktur des Codes. Nur die Geschwindigkeit, mit der dieser Hebel wirkt, ist gewachsen.

Strategische Relevanz: Taktik nur für das Herzstück

Die bisherige Argumentation verdeutlicht: Softwareentwicklung ist ein evolutionärer Prozess, der von ständiger Anpassung und der permanenten Prüfung (insbesondere auch der Prüfung von abstrakten Qualitätsparametern wie Anpassbarkeit, Erweiterbarkeit oder Wartbarkeit) lebt. Damit dieser Prozess nicht im Chaos versinkt, muss der Code eine Struktur aufweisen, die Anpassbarkeit, Erweiterbarkeit und Nachvollziehbarkeit ermöglicht. Doch hier ist Differenzierung gefragt, denn nicht jeder Code verdient den gleichen Grad an gestalterischer Akribie.

Im Kontext von Domain-Driven Design unterscheiden wir zwischen verschiedenen Subdomains. Während Generic Subdomains (z. B. Finanzbuchhaltung) oft mit Standardlösungen oder Supporting Subdomains (z. B. einfache Daten-CRUD-Module) mit einfacheren Mitteln wie Low-Code-Tools abgedeckt werden können, ist die Core Subdomain das eigentliche Herzstück. Hier liegt die Logik, die den Wettbewerbsvorteil des Unternehmens ausmacht.

Genau hier entfaltet taktisches DDD seine volle Stärke. Es eignet sich besonders für

  • Komplexe Businesslogik: Systeme mit tief verschachtelten Regeln und hohen Anforderungen an die Datenintegrität.
  • Langlebige Software: Projekte, die über Jahre gewartet und deren Fachmodelle stetig erweitert werden müssen.
  • Kritische Systeme: Überall dort, wo ein falscher Zustand (Invalid State) fatale wirtschaftliche oder technische Folgen hat.

Konkrete Anwendungsfelder, in denen sich der Einsatz immer wieder bewährt hat:

  • Finanzsysteme: Sicherstellung von Transaktions-Invarianten, die niemals durch KI-generierte Seiteneffekte korrumpiert werden dürfen.
  • Logistik-Plattformen: Modellierung von hochdynamischen Zuständen, bei denen die Konsistenz über Zeit- und Ortsdaten nur durch strikte Aggregate-Grenzen gewährleistet werden kann.
  • Versicherungs-Engines: Abbildung komplexer Tarife, bei denen die Fachlogik so tief im Modell verwoben ist, dass eine einfache CRUD-Struktur sofort zu teuren Berechnungsfehlern führen würde.
  • E-Government: Absicherung formal-juristischer Prüflogiken. Taktisches DDD verhindert, dass KI-generierter Code rechtsverbindliche Invarianten in Verwaltungsverfahren (z. B. Bewilligungskriterien) durch unzulässige Heuristiken aufweicht.
  • Energy & Utilities: Verwaltung dezentraler Energieströme und Bilanzkreise. Hier garantieren klare Domänengrenzen, dass KI-optimierte Laststeuerung nicht gegen physikalische Netzrestriktionen oder komplexe regulatorische Abrechnungsmodelle verstößt.

Taktisches DDD ist jedoch keine „Silver Bullet”. Für einfache Web-Apps, reine Daten-Pipelines oder Prototypen mit kurzer Lebensdauer kann der Overhead von Aggregates und Value Objects zu groß sein. In solchen Fällen ist ein simplerer, datenzentrierter Ansatz oft wirtschaftlicher. Wer DDD auf ein triviales Problem wirft, baut eine „Over-Engineering”-Falle, in der die Komplexität des Designs die Komplexität des Problems übersteigt.

Drei Hebel: Wie taktisches DDD den Copiloten führt

Taktisches DDD adressiert die typischen Schwächen generativer Erzeugung durch drei Klassen von Mechanismen: strukturelle, semantische und absichernde, die jeweils ein anderes Risiko der KI-gestützten Entwicklung abfangen.

  • Strukturelle Mechanismen: Modularisierung statt Vermischung. Ohne explizite strukturelle Vorgaben füllt die KI das Design-Vakuum mit dem, was statistisch wahrscheinlich ist, oft prozedurale Klassen mit vermischten Verantwortungen und enge, starre Abhängigkeiten zwischen fachlich eigentlich unabhängigen Prozessen. Aggregates erzwingen fachliche Modularisierung und definieren klare Verantwortungsgrenzen; Domain Events brechen starre Strukturen auf, indem Folgeprozesse nicht direkt aufgerufen, sondern als isolierte Handler auf veröffentlichte Ereignisse (z. B. OrderPlaced) reagieren. Side-Effect-Free Functions, also nebenwirkungsfreie Methoden, die nur Werte zurückgeben, ohne den Zustand des Aggregates zu verändern, trennen Berechnung von Zustandsänderung (Command-Query Separation) und verhindern, dass die KI Logik und Seiteneffekte zu unentwirrbaren Methoden vermischt. Der Begriff stammt von Eric Evans und beschreibt eine zentrale Grundhaltung des taktischen DDD: Wo es möglich ist, soll Verhalten vorhersagbar bleiben, indem es nichts verändert. Aus genau dieser Haltung leiten sich auch Value Objects ab, die ihre Unveränderlichkeit nicht zufällig haben, sondern als konsequente Anwendung desselben Prinzips. Diese strukturelle Klarheit hat einen praktischen Nebeneffekt: Da Fachlogik in dedizierten Bausteinen gekapselt ist, muss dem Copiloten nur ein kleiner, hochrelevanter Codeausschnitt präsentiert werden. Das minimiert „Lost-in-the-Middle”-Effekte und Halluzinationen.
  • Semantische Mechanismen: Bedeutung statt generischer Begriffe. KI greift ohne Steuerung auf technische Primitive und generische Methodennamen zurück. Drei Prinzipien wirken dem entgegen: Die Ubiquitous Language sorgt dafür, dass die Fachsprache konsequent bis in den Quellcode getragen wird. Die KI verwendet ClaimAdjustment statt updateStatus. Intention-Revealing Interfaces benennen den Zweck einer Operation so, dass die fachliche Absicht ohne Implementierungsblick erkennbar ist (acceptTermsAndConditions() statt processData()) – das schafft die Erwartungshaltung, gegen die der Reviewer die KI-Lösung sofort prüfen kann. Value Objects eliminieren die sogenannte Primitive Obsession: Wenn IBAN und String verschiedene Typen sind, kann die KI sie nicht mehr verwechseln, und Geschäftsregeln wandern in das Typsystem selbst.
  • Absichernde Mechanismen: Garantien statt Hoffnung. Statt darauf zu vertrauen, dass die KI fachliche Regeln korrekt umsetzt, werden diese als harte Zusicherungen im Code verankert. Assertions formulieren Invarianten explizit. Der Reviewer erkennt sofort, welche Garantie der Code gibt, und Verstöße fallen in Tests früh auf. Der entscheidende Zweitnutzen bei KI-Unterstützung: Bei späteren Erweiterungen liest die KI diese Invarianten automatisch aus dem Code, ohne dass sie jedes Mal im Prompt mitgegeben werden müssen. Die Logik schützt sich selbst vor unbedachten Änderungen durch den Copiloten.
  • Strukturelle Erwartungshaltung als gemeinsamer Effekt: Diese drei Mechanismen wirken zusammen wie ein Filter für den Review. Jeder DDD-Baustein definiert eine spezifische Erwartung. In einem Value Object ausschließlich Berechnungen und Validierungen, in einem Aggregate die Durchsetzung von Invarianten, in einem Application Service nur Koordination. Sobald die KI dieses Profil verletzt, etwa eine Datenbankverbindung in ein Value Object mischt, wird die strukturelle Abweichung sofort sichtbar. Das Design fungiert damit als Aufmerksamkeitslenker: Sie nimmt dem Reviewer die Arbeit ab, alles prüfen zu müssen, und fokussiert ihn auf die fachliche Korrektheit dort, wo sie wirklich entschieden wird.

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Fazit: Design als Überlebensstrategie im KI-Zeitalter

Wer den Copiloten in der Core Domain von der Leine lässt, ohne ihm die strukturellen Vorgaben des taktischen DDD (oder anderer detaillierter Designprinzipien) zu geben, baut unter Umständen in Rekordgeschwindigkeit eine technische Sackgasse. Wir haben gesehen, dass KI inzwischen vieles beherrscht, was vor wenigen Jahren noch als Grenze galt: komplexere Reasoning-Ketten, präzisere API-Nutzung, autonomes Arbeiten in größeren Kontexten. Was sie weiterhin nicht hat, ist ein Bewusstsein für architektonische bzw. Design Integrität, langfristige Änderbarkeit und fachliche Invarianten.

Hochwertiges Softwaredesign ist daher im Zeitalter der generativen KI kein optionales Extra für Ästheten, sondern ein zentraler wirtschaftlicher Hebel. Taktisches DDD wirkt dabei auf drei Ebenen:

  • Es macht generierten Code durch klare Strukturen deutlich besser prüfbar.
  • Es sichert die Integrität des Kern-Geschäftsmodells gegen statistische Unschärfen ab.
  • Es garantiert, dass wir auch morgen noch Verantwortung für unsere Systeme tragen können, statt im Schuldenberg KI-generierten Codes zu versinken.

Die Rolle des Entwicklers wandelt sich dabei grundlegend: Er schreibt weniger Zeilen, trägt aber eine höhere Verantwortung für die strukturelle Korrektheit. Und je autonomer der Copilot wird, desto mehr entscheidet die Qualität des Softwaredesigns darüber, ob seine Geschwindigkeit zum Vorteil oder zum Problem wird. Wer diese Leitplanken beherrscht, nutzt die KI nicht nur als Schreibhilfe, sondern als hocheffizienten Werkzeugmacher für ein robustes digitales Fundament.

Von der Theorie in die Praxis. Wie diese theoretischen Bausteine in einem modernen Tech-Stack konkret zum Leben erweckt werden, zeigen die Folgeartikel dieser Serie. Teil 2 übersetzt anhand eines praktischen Beispiels das fachliche Modell in Code und zeigt, welche Eigenschaften der taktischen DDD-Bausteine sie zur belastbaren Grundlage für KI-gestützte Entwicklung machen. Die Teile 3 und 4 schließen den Bogen zum Werkzeug: Wie wird das Softwaredesign als persistente Vorgabe im Projekt verankert, wie sehen konkrete Aufträge an den Copiloten aus, und wie macht ein automatisch erzeugtes Strukturdiagramm sichtbar, was die KI tatsächlich gebaut hat?


Links & Literatur

[1] Evans, Eric: „Domain-Driven Design: Tackling Complexity in the Heart of Software”, Addison-Wesley, 2003

[2] Atıl, Berk et al.: „Non-Determinism of “Deterministic” LLM System Settings in Hosted Environments” in: Proceedings of the 5th Workshop on Evaluation and Comparison of NLP Systems (Eval4NLP), S. 135–148, Mumbai, Indien. Association for Computational Linguistics, 2025: https://aclanthology.org/2025.eval4nlp-1.12/

[3] Salvatore, Nikolaus; Wang, Hao; Zhang, Qiong: „Lost in the Middle: An Emergent Property from Information Retrieval Demands in LLMs”, arXiv:2510.10276, 2025: https://arxiv.org/abs/2510.10276

[4] Song, Peiyang; Goodman, Noah et al.: „Large Language Model Reasoning Failures”, arXiv:2602.06176, 2026

The post Taktisches DDD für wartbaren KI-generierten Code appeared first on JAX.

]]>
AI braucht Design: Die Rolle von Spezifikationen in Enterprise AI https://jax.de/blog/enterprise-ai-braucht-design-spezifikationen-softwareentwicklung/ Fri, 12 Jun 2026 10:05:17 +0000 https://jax.de/?p=210525 KI macht Softwareentwicklung schneller – aber nicht automatisch besser. Der Artikel zeigt, warum präzise Spezifikationen und gutes Design zur Grundlage verlässlicher Enterprise-AI-Systeme werden.

The post AI braucht Design: Die Rolle von Spezifikationen in Enterprise AI appeared first on JAX.

]]>
„Design before Implementation” gehört zu den zentralen Prinzipien guter Softwareentwicklung – und wurde in vielen Projekten in den letzten Jahren vernachlässigt. Mit dem Aufkommen von AI-gestützter Entwicklung entsteht nun oft der Eindruck, man könne aus lose formulierten Geschäftsregeln direkt funktionierende Software erzeugen. Das Gegenteil ist jedoch der Fall: Gerade wenn AI Teil realer Anwendungen wird, steigen die Anforderungen an Klarheit, Struktur und präzise Entwürfe. Der Beitrag zeigt, warum detaillierte Spezifikationen und sauberes Systemdesign nicht an Bedeutung verlieren, sondern im Zeitalter von Enterprise AI wichtiger werden als je zuvor.

Um es gleich vorwegzusagen: Von sauberem Design profitiert jede Softwareentwicklung, ob mit oder ohne KI. Wenn dafür eine „Renaissance“ entsprechender Prinzipien notwendig ist, dann ist sie ohnehin überfällig und der Zeitpunkt perfekt. In vielen Projekten, die nach iterativen/agilen Verfahren arbeiten, sind klassische Techniken leider auf der Strecke geblieben. Design gehört aus zwei Gründen dazu.

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Zum einen, weil „agil“ gern als Ausrede für das Weglassen jeglicher Dokumente – also auch jeglicher Designdokumente – herhalten musste. Das ist eine Missinterpretation, die letzten Endes den Begriff „Software Craftsmanship“ hervorgebracht hat [1]. Abläufe und Strukturen in nicht trivialer Software bedürfen detaillierter Beschreibungen, die mit dem Auftraggeber abgestimmt sind. Was dem Handwerker seine Bauzeichnung ist und dem Musiker seine Partitur, ist dem Software Craftsman seine Softwarespezifikation.

Zum anderen – und das macht auch Anhängern des präzisen Entwurfs das Leben schwer –, weil es kaum ein Werkzeug gibt, das sich komfortabel für eine iterative Entwicklung der Entwürfe eignet. Zum Beispiel hat kein einziges bekanntes UML-Tool einen Änderungsmodus mit einer hybriden Darstellung von bestehendem, neuem und gelöschtem Inhalt wie ihn etwa Microsoft Word hat. Das ist ein Hauptgrund, warum MS Word über Jahrzehnte hinweg Grundlage für iterative Systemspezifikationen war. Andererseits ist Word kein Modellierungswerkzeug, sondern „nur“ ein Textverarbeitungsprogramm. Wenn eine KI die Kontrollstrukturen einer Ablaufbeschreibung verstehen soll – Schleifen, Verzweigungen, Ausnahmen usw. –, dann ist eine Freitextbeschreibung die falsche Basis.

Renaissance heißt also nicht einfach zurück zu den alten Methoden und Werkzeugen, sondern die Prinzipien des Software-Engineerings, die sich über Jahrzehnte bewährt haben, mit einer neuen Generation von Werkzeugen umzusetzen, die den heutigen Anforderungen an Agilität und KI-Integration gerecht werden. Selbst Vibe Coder merken das zunehmend und bringen den Begriff „Spec-driven Development“ als Trendthema auf [2], wobei dort aktuell andere Schwerpunkte gesetzt werden.

Warum Design wichtig ist

Mit Design bzw. Entwurf ist hier primär gemeint: eine detaillierte Beschreibung von Abläufen in den System-Use-Cases einer Software. Die Beschreibungen sind einerseits möglichst fachorientiert und natürlichsprachlich formuliert, andererseits aber so präzise, dass sie ohne Risiko von Fehlinterpretationen in Code übersetzt werden können. Sie fußen dabei auf einem Vokabular von Geschäftsobjekten und deren Beziehungen untereinander. Beides – die Geschäftsobjekte und die Abläufe – sind verbindliche Grundlagen für alle Stakeholder, vom Auftraggeber über die Entwickler bis zu den Testern. Das sind seit jeher zentrale Konzepte von OOA/OOD und Domain-Driven Design. Wenn es um UI-Funktionalität geht, kommen außerdem Oberflächenspezifikationen hinzu (z. B. Wireframes), die in diesem Artikel aber außen vor bleiben.

Solche Softwareentwürfe sind in ihrer Funktion einer Bauzeichnung beim Bau oder Umbau eines Hauses nicht unähnlich. Und wer jetzt einwendet, dass Häuser im Gegensatz zu Software nicht ständigen Änderungen unterliegen, der möge an Solaranlagen, Wallboxen, Wärmepumpen, Glasfaser usw. denken. Ein Segen, wenn dann gute, aktuelle Grundrisse und Leitungspläne des Gebäudes vorliegen. Gerade iteratives Vorgehen profitiert von jederzeit aktuellen Entwürfen.

Abb. 1: User Story für die Altersdarstellung von Chatbeiträgen

Abb. 2: Systemspezifikation für Altersdarstellung als „Aktogramm“

Zur Verdeutlichung zeigt Abbildung 1 ein einfaches Beispiel für eine Handvoll Geschäftsregeln, wie sie in einer User Story stehen könnten, und Abbildung 2 eine daraus entworfene Ablaufbeschreibung in einer Notation, die später noch genauer betrachtet wird. Die Entwurfsform und das Software-Engineering dahinter sind bewährte Konzepte, die auch in hochkomplizierten Szenarien Anwendung finden. In dem Trivialbeispiel aus Abbildung 1 geht es um die leserfreundliche Darstellung des Alters von Beiträgen in einem Chat. Statt 3 621 Sekunden seit der Veröffentlichung soll dort z. B. „1 Stunde“ stehen. Völlig unkritische Funktionalität, aber es könnten natürlich auch die Ratingregeln einer Bilanzanalyse sein, die in einer Bank über die Vergabe von Millionenkrediten entscheiden.

Was passiert nun, wenn ein unbedarfter Entwickler direkt aus den Geschäftsregeln heraus Code produziert? Die Anforderungen klingen einfach, also wird er direkt drauflos programmieren, ist aber natürlich trotzdem gezwungen, sich Gedanken über die Struktur des Codes zu machen. Die Geschäftsregeln sagen ja nur, was zu tun ist, aber nicht wie. Was sich jetzt abspielt, nennt sich „design a little, code a little“ – ein Antipattern! Unter [3] kann man nachlesen, wieso es anti ist. Es entstehen schlechtes Ad-hoc-Design und schlechter Code. Und was ändert sich, wenn man stattdessen mit einem KI-Agenten arbeitet? Man bekommt den schlechten Code schneller geliefert, das ist alles.

Enterprise AI & Agentic Systems

Enterprise AI-Track entdecken

 

Bei einem KI-Coding-Dojo im vergangenen Herbst durften sich Github Copilot und Junie mit verschiedenen LLMs an dieser Aufgabe versuchen, ohne dass dabei Code entstand, der durch weiteres Prompting irgendwann eine vernünftige Gestalt angenommen hätte. Man wird also im Zeitraffer direkt Zeuge des Problems, und die KI-Agenten zeigen uns im Grunde nur: So naiv geht es nicht. Dabei ist noch gar nicht berücksichtigt, dass die Fachbereiche ihre Anforderungen eher „in loser Schüttung“ liefern – ohne Anspruch auf Vollständigkeit und Schlüssigkeit, dafür mit unausgesprochenen Details zwischen den Zeilen.

Ganz anders sieht das Ergebnis basierend auf der Ablaufspezifikation in Abbildung 2 aus. Hier ist der Entwurf in einer separaten Phase vorweg entstanden und wurde so beschrieben, dass alle Beteiligten das Prozedere der Software nachvollziehen können. Die Beschreibung geschieht in einer Weise, dass man quasi bei Ausfall der Software einen Sachbearbeiter hinsetzen könnte, damit er dem Ablauf folgend die Arbeit manuell erledigt, wenn auch eine Milliarde Mal langsamer. Sowohl Entwickler als auch KI-Agenten können auf dieser Basis brauchbaren Code erstellen, der sich an der Struktur des spezifizierten Ablaufs orientiert und somit auch leicht darauf zu prüfen ist, ob er überhaupt das tut, was gefordert ist. Bei späteren Änderungen im Rahmen iterativer Entwicklung sind die in der Spezifikation vorgenommenen Modifikationen entsprechend leicht im Code zu verorten.

Hier spielen gleiche mehrere Faktoren eine Rolle, die eine vorgelagerte Designphase so wertvoll machen, die sich in späteren Phasen auszahlt:

  • Klarheit des Entwurfs: Wenn man sich zwingt, ein für „Normalsterbliche“ nachvollziehbares Verfahren zu beschreiben, das das fachlich gewünschte Ergebnis erzielt, dann stellt man häufig fest: Die vermeintlich bereits glasklaren Gedanken dazu sind unklarer, als man glaubte. Fast jeder kennt das: Sobald man versucht, jemand anderem zu erklären, was man vorhat, zerbröselt eine undurchdachte Idee zu Staub. Eine gute Idee hingegen wird dadurch gestärkt und verfeinert.
  • Analytisches Denken: Es dauert u. U. ziemlich lange, bis man ein leicht zu verstehendes Vorgehen formuliert hat, und das ist gut so. Es zwingt den Entwickler, aus dem schnellen impulsiven Denken in das langsame analytische Denken zu wechseln. Es werden dabei ganz andere Hirnregionen aktiv [4] und das Ergebnis ist ein völlig anderes als bei „design a little, code a little“.
  • Kurze Feedbackschleifen: Sind die Entwürfe für alle lesbar, können auch die Fachanforderer sie beurteilen und dazu beitragen. Hier entstehen schnelle Feedbackschleifen, von denen agile Entwicklung ohne Spezifikationen nur träumen kann. Schwere Missverständnisse und Konzeptfehler fallen nicht erst nach zwei Wochen in der Sprintreview auf, sondern schon nach zwei Stunden in der frühestmöglichen Phase der Softwareentwicklung. Die Rule of Ten [5] erlaubt da schon viel Spezifikationsarbeit, bevor eine Überfrachtung entsteht.

Um es klar zu betonen: Es geht nicht um eine Rückkehr zum Wasserfallmodell! Die Entwürfe müssen iterativ weiterentwickelt werden können. Sie sind keine Wegwerfprodukte aus einer initialen Anstrengung, sondern lebende Artefakte des Software-Engineerings. Das funktioniert nur, wenn sie in geeigneten Werkzeugen erstellt werden und tagtäglich Mehrwert schaffen. Das ist eine der Herausforderungen, die sich bewusst zu adressieren lohnt.

Entwürfe MIT statt VON der KI

Entwurfsarbeit ist soooo anstrengend – kann das nicht die KI machen? Kann sie nicht, denn sonst wäre ja in dem oben erwähnten Dojo vom Fleck weg guter Code entstanden. Sie ist Konsument der Entwürfe. Die Diskussionen, ob und wie weit KI auch Entwürfe herstellen kann, füllen derzeit die Kaffeeküchen aller IT-Unternehmen der Welt.

Die in diesem Artikel vertretene Meinung fußt auf folgender Sichtweise: Der Abschnitt zuvor erwähnt die Feedbackschleifen, die einen guten Entwurf ausmachen. Menschen, die solche Entwurfsarbeit lieben, streben dabei nach Erkenntnisgewinn und nach einem gemeinsamen Verständnis der Welt zwischen den Beteiligten. In ihren Entwürfen sind sie darauf aus, fachliche Notwendigkeiten und technische Möglichkeiten optimal auszubalancieren. Sie treiben diesen Vorgang bis zu einem Punkt, an dem sie sicher sind, dass die folgende Umsetzung kein Abenteuer mehr ist, sondern zielgerichtetes Handwerk mit einem vorhersagbaren Ergebnis. Software Craftsmanship eben.

Künstliche Intelligenz heutiger Bauart hingegen will auf gar nichts hinaus. Es ist eine hochentwickelte Mustererkennungsmaschine, der wir Menschen ihrer guten Ausdrucksweise wegen gerne Intelligenz, Empathie und intrinsische Motivation zuschreiben. Das ist eine fundamentale Denkfalle, so alt wie der Begriff „künstliche Intelligenz“ selbst [6]. Um KI in der Entwicklung komplizierter Softwaresysteme zu nutzen, muss sie mit klaren Mustern und Strukturen gefüttert werden. Das Ziel ist schließlich keine grob in die richtige Richtung gehende „interessante Inspiration“, sondern exakter, bis auf die letzte 0 und 1 stimmiger Code. Mit „exakt“ haben es LLMs aber naturbedingt nicht so, weshalb die Klarheit der Strukturen und Muster umso wichtiger wird. Und wo kommt diese Klarheit her? Entwurfsarbeit!

Entwürfe sind von der KI also nicht zu erwarten, was eigentlich eine gute Nachricht ist: Wer sich auf diese Tätigkeit versteht – tendenziell also jeder Clean Coder – wird so schnell nicht arbeitslos. Es geht wohlgemerkt um komplizierte Softwaresysteme mit Hunderttausenden Zeilen. Energiewesen, Bilanzanalyse, Touristik und Kundenbindungsprogramme sind einige der Umfelder, in denen die hier dargestellten Konzepte Anwendung finden. Abbildung 3 zeigt bespielhaft aus der Vogelperspektive eine vollständige Use-Case-Spezifikation mittlerer Größe, um einen Eindruck zu vermitteln, wie so etwas in echten Projekten aussieht. Die Ablaufbeschreibung ist hier eingebettet in eine Präambel, Vor- und Nachbedingungen, Geschäftsklassendiagramme, Wireframes usw. Auch wenn man die Texte in dieser Auflösung nicht lesen kann, sollte deutlich werden, dass hier keine triviale Logik beschrieben ist. Ohne Spezifikation ist eine permanente Fortentwicklung solcher Use Cases viel zu ineffizient. Natürlich lässt sich die Arbeit an großen Entwürfen prima mit KI unterstützen, wie Matthias Bartels in seinem Artikel „Mit automatisierter Review zum gelungenen Entwurf“ in dieser Ausgabe des Java Magazins zeigt [7]. Um die zentralen Aspekte zu erläutern, soll aber im Weiteren das oben vorgestellte kleine Beispiel genügen.

Abb. 3: Echte Use-Case-Spezifikation mittlerer Größe, Vogelperspektive

Der eigene Kopfcomputer bleibt also der Ort, wo die Entwürfe letztlich entstehen, wo sie aber nicht bleiben dürfen. Machen wir sie Mensch und Maschine zugänglich, dann maximieren wir die Möglichkeit der Zuarbeit von beiden Seiten. An dieser Stelle kommt passendes Tooling ins Spiel. Künstliche Intelligenz hat zwar erstaunliche Fähigkeiten, fast beliebigen Input zu verarbeiten. Die Qualität der Ergebnisse hängt aber doch davon ab, wie präzise die Eingaben strukturiert und formuliert sind.

Abb. 4: Systemspezifikation für Altersdarstellung in Microsoft Word

Abbildung 4 zeigt eine alternative Darstellung der Spezifikation aus Abbildung 2, basierend auf einem viel genutzten Microsoft-Word-Template, wie eingangs schon erwähnt wurde. Es soll beispielhaft die Problematik zeigen, die entsteht, wenn man bequem greifbare Text- und Maltools zum Modellieren missbraucht und damit der KI und auch jeder anderen maschinellen Unterstützung das Interpretieren schwer macht. Was direkt auffällt: Die in Abbildung 2 auf den ersten Blick erkennbaren Verzweigungen (dargestellt durch Diamanten) sind nicht mehr unmittelbar zu sehen. Sie liegen textuell beschrieben vor, was dem tabellarischen Layout geschuldet ist, um automatische Nummerierung und Änderungsstabilität zu gewährleisten. Heftige Kompromisse also, die für menschliche Leser vielleicht ein verkraftbares Problem sind. Aber ob eine KI daraus die gemeinte Ablaufstruktur noch gut herauslesen kann? Noch ungünstiger wird es, wenn für Remotearbeit auf kostengünstige, webbasierte Kollaborationswerkzeuge zurückgegriffen wird (z. B. Confluence). Für die Modellierung sind diese Tools nicht gedacht.

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Wie sieht’s mit UML-Aktivitätsdiagrammen oder BPMN aus? Hier lassen sich Abläufe modellieren und damit auch gut maschinell auswerten. Leider tauscht man damit die Vor- und Nachteile gegenüber MS Word quasi eins zu eins aus. Änderungsverfolgung, automatische Nummerierung und änderungsstabiles Layout sind futsch. Davon abgesehen brauchen diese Notationen viel Platz und vertragen kaum Text für notwendige Details, die man stattdessen in eine verborgene Doku oder angeheftete Textblasen auslagern muss. Also wieder reichlich Kompromisse in anderer Form, wie in Abbildung 5 exemplarisch dargestellt ist. Das sind alles keine unüberwindbaren Hürden und keine Ausrede dafür, ohne Entwürfe zu arbeiten. Aber vielleicht ist es für eine neue Mensch-Maschine-Zusammenarbeit beim Entwerfen Zeit für „New Kids on the Block“ …

Abb. 5: Systemspezifikation für Altersdarstellung als UML-Aktivitätsdiagramm

Aktogramme

Abbildung 2 zeigt, wie wir gesehen haben, eine Spezifikation für das Beispiel der Altersdarstellung von Chatbeiträgen in Form eines „Aktogramms“. Es handelt sich um eine für Spezifikationen weiterentwickelte Form von Struktogrammen – auch als Nassi-Shneiderman-Diagramme bekannt [8]. Sie sind noch älter als die erste Erwähnung von „Design before Implementation“, dienten aber genau diesem Prinzip. Dabei lag ihr Fokus seinerzeit auf der strukturierten Darstellung von Programmcode, als Letzterer noch 70er-Jahre-Assembler war (so alt sind Struktogramme).

DIE KUNST DER SOTWARE-ARCHITEKTUR

Architecture & Design-Track entdecken

 

Dieses Problem ist mit Hochsprachen und IDEs mit Syntax-Highlighting inzwischen besser gelöst. Für Spezifikationen werden sie aber wieder interessant, wenn man sie ein bisschen aufmöbelt. Der Begriff „Aktogramm“ ist aus „Struktogramm“ und „Aktivitätsdiagramm“ zusammengesetzt, weil die Notation im Kern dem Darstellungskonzept von Struktogrammen folgt, aber einige Anlehnungen an UML-Aktivitätsdiagramme aufweist. So zum Beispiel die in Abbildung 2 gut erkennbaren Diamanten als Ausdrucksform für Verzweigungen, weil die originale Dreiecksform von Struktogrammen unordentlich wirkt, wenn sie in einem Ablauf wiederholt auftaucht. Es steckt also einiges an Feinschliff in der Notation, um sie für Spezifikationen größeren Umfangs tauglich zu machen. Schaut man sich das Aktogramm aus Abbildung 2 und das entsprechende UML-Aktivitätsdiagramm aus Abbildung 5 aus größerer Entfernung an, bemerkt man die beiden Diamanten recht gut als Gemeinsamkeit.

Das Aktogramm wurde mit dem Java-WYSIWYG-Editor Specman [9] erstellt, der verschiedene praktische Eigenschaften von Word und UML zusammenbringt, z. B.

  • Änderungsstabiles Layout: Wenn man hier Schritte in den Ablauf einbringt oder entfernt, gerät das Layout nie durcheinander wie bei Aktivitätsdiagrammen
  • Änderungsmodus: Wie in Word, aber mit einer deutlicheren farblichen Hervorhebung wie mit einem Textmarker (3); man scrollt einfach durch und schaut sich eine gelbe Markierung nach der nächsten an, um die am Use Case vorgenommenen Änderungen zu erkennen
  • Automatische Schrittnummerierung: Sehr wichtig für Abstimmungsprozesse; „Schau dir mal Schritt 3.4.2 an, ob ich dich gestern richtig verstanden habe“, ohne Nummern wird das Absprechen komplizierter Abläufe schwierig
  • Schrittreferenzen: Automatisch aktualisierte Referenzen auf die Nummern anderer Schritte in den Beschreibungen; in Word Usus, in UML ein Manko
  • Viel Inhalt: Wir können uns nicht aussuchen, ob die Geschäftslogik einfach oder kompliziert ist, und die Notation darf uns im Fall der Fälle nicht beschränken; in Specman kann man aber jeden untergliederten Schritt (Schleifen, Verzweigungen, Untersequenzen) in der Ansicht zusammenklappen, wenn man an deren Inhalt gerade nicht interessiert ist
  • Kontrollstrukturen: Sind gut erkennbar – nicht so gut wie in UML, aber viel besser als in Word; hinzu kommt eine wichtige Erweiterung gegenüber Struktogrammen: Letztere kennen zwar sogenannte Breaks als Entsprechung zum Werfen einer Exception, aber es fehlt die Entsprechung für das Fangen; in Spezifikationen ist es aber wichtig, wie das System mit fachlich relevanten Ausnahmen umgeht, also nicht mit NullPointerException und OutOfMemory– das gehört als querschnittliches Verfahren in die Architekturdoku –, sondern mit Fällen wie „Kunde ist gesperrt“ oder „Zahlungsart ungültig“

Specman speichert die Diagramme in einem JSON-Format, aus dem eine KI die Struktur des Ablaufs gut ablesen kann. Muss sie so etwas aus Bildern ablesen, führt das zu einer höheren Anfälligkeit für Fehlinterpretationen und kostet wesentlich mehr Zeit (und Tokens).

Beim Coding auf Grundlage von Aktogrammen können KI und menschliche Entwickler einige Aspekte direkt übernehmen. Ein Beispiel ist die Unterstrukturierung, die man beim Entwurf komplizierter Abläufe in Aktogrammen automatisch vornimmt. Sie lässt sich bei der Implementierung für das Designprinzip „Single Level of Abstraction“ nutzen. Ein erneuter Blick auf Abbildung 2 zeigt beispielsweise, welche Schritte die oberste Spezifikationsebene bilden – die Schritte mit einstelliger Schrittnummer. Man darf also erwarten, dass auf der obersten Codeebene für die Funktionalität eine Abfolge von Funktionsaufrufen zu finden ist, die genau das widerspiegelt:

  • Absicherung gegen negative Angaben
  • Bildung des Stundenanteils
  • Bildung des Minutenanteils
  • Bildung des Sekundenanteils
  • Zusammenstellen des Ergebnisses aus den Anteilen

Es stellt sich für den Entwickler nicht die Frage, ob das eine funktionierende und sinnvolle Strategie zur Erfüllung der Anforderungen ist. Das wurde im Vorfeld in der Entwurfsphase ausgetüftelt. Idealerweise unter seiner direkten Mitarbeit oder sogar Federführung, denn die Entwürfe sind nur brauchbar, wenn sie mit technischem Sachverstand entstehen. Dass Fachbereiche oder Businessanalysten das zukünftig selbst machen und dann nur noch eine KI anwerfen, ist also unwahrscheinlich.

Speckit und Co.

Ausgerechnet die junge Szene der Vibe Coder [10] bringt das Thema Spezifikation im Zusammenhang mit KI wieder ins breite Bewusstsein. Trotz zweifellos höherer Schmerzresistenz bzgl. der Codequalität sind die Erkenntnisse dort offenbar die gleichen wie in dem oben beschriebenen KI-Dojo: Von Hello-World-Programmen abgesehen führt Coding ohne Design innerhalb kürzester Zeit in die Unwartbarkeit. Also entstehen auch in diesem Umfeld Spezifikationswerkzeuge, die allerdings nicht den Anspruch haben, die Entwürfe für alle Stakeholder lesbar zu machen. Speckit [11] ist so ein Werkzeug, das eine Plaintext-basierte Notation verwendet. Die Zugänglichkeit aus einer IDE oder Kommandozeile heraus genügt, weil es nur um verbesserte Kommunikation zwischen Entwickler und KI geht. Andererseits reicht der Anspruch in Richtung Coding so weit, dass die KI auf reproduzierbare Weise vollständige Applikationen implementieren soll.

Der Verbrauch kostbarer KI-Tokens gilt bei Speckit allerdings als enorm, was die leichtgewichtigere Alternative OpenSpec [12] zu verbessern verspricht. Dafür hat Letztere aber noch weniger mit Spezifikationen gemeinsam, wie sie in diesem Artikel gemeint sind. Es geht hier eher um die Beschreibung von Arbeitsaufträgen an die KI. Immerhin entsteht auf diese Weise Wiederholbarkeit und Nachvollziehbarkeit, statt dass der Code über geheimnisvolles Ad-hoc-Prompting immer wieder überraschend anders entsteht [13].

Nassi

Plaintext-basierte Spezifikation und hoher Detailgrad sind übrigens kein Widerspruch. Neben Specman gibt es den Aktogramm-Editor Nassi [14], der einen solchen Weg verfolgt und die für Fachanforderer verständlichen Diagramme in HTML-Form generiert. Dieser andere Ansatz für die Erstellung der Aktogramme führt zu einigen Unterschieden darin, wie sich der Spezifikationsvorgang in das Software-Engineering integriert. Im Kern verfolgt Nassi aber dieselbe Intention wie Specman.

In seinem Artikel „Aktogramme als Kommunikationsmedium“ in dieser Ausgabe des Java Magazins stellt Jan Hermanns den Editor ausführlich vor und geht außerdem tiefer auf iterative Entwurfsarbeit ein [15]. Beide Editoren sind Open Source.

KI auf Aktogrammen

Mit maschinenlesbaren, feingranularen und klar strukturierten Entwürfen kann die KI nun auf vielfältige Weise unterstützen, ohne dabei ausgeprägt in die Irre zu laufen oder zu halluzinieren.

Das naheliegende erste Einsatzgebiet ist die Validierung der Spezifikation selbst. Nicht so sehr im Sinne von: Beschreibt die Spezifikation das, was gebraucht wird? Das zu entscheiden ist Aufgabe der Fachbereiche im Rahmen ihrer Reviews. Deswegen ist gute Lesbarkeit für uns Menschen so wichtig. Eine KI kann aber prüfen, ob die Abläufe in sich logisch konsistent und vollständig sind oder ob sie „lose Enden“ haben. Matthias Bartels geht darauf in seinem Artikel ein [7].

Auch die Validierung von Code gegen die Spezifikation – ein zentraler Bestandteil jeder Code-Review – lässt sich teilautomatisieren. Wenn sich der Code sauber an der Struktur der Spezifikation orientiert, dann ist eine KI wie beispielsweise Claude Code ziemlich gut in der Lage, die Übereinstimmung zu prüfen.

Matthias Bartels vertieft auch diesen Aspekt in seinem Artikel, da er ein wichtiges Sicherheitsnetz für die Frage aller Fragen ist: Kann die KI aus genügend präzisen Beschreibungen zielgerichtet Code generieren, der exakt die spezifizierte Funktionalität realisiert? Vorzugweise, ohne dass die Spezifikation dafür den Umfang des Codes selbst annimmt.

Ein klares „Jein“

Es müssen Stand heute noch viele zusätzliche Bedingungen geschaffen werden, um in komplizierten Systemen nennenswert Code aus Entwürfen generieren zu können. Neben der Spezifikation der Geschäftsobjekte (siehe unten) braucht KI noch eine ganze Menge mehr saubere Strukturen – und zwar in der Architektur. Wenn die KI auf eine Weise coden soll, dass es in eine größere Codebasis passt, dann muss sie darin Muster für alles vorfinden, was es zu coden gibt. Sie verwendet sonst munter alles durcheinander, wovon sie in ihrem angelernten projektfremden Wissen meint, dass es passen könnte. Wenn also im Code keine Ordnung herrscht, dann beschleunigt die KI das Broken-Window-Problem [16], und die Unordnung wächst noch schneller als mit menschlichen Entwicklern.

In einer unordentlichen Codebasis empfiehlt es sich, zunächst eine „Insel der Ordnung“ zu schaffen, an der sich die KI orientieren kann. Ein erfolgreich praktizierter Ansatz besteht darin, in einem verlebten Bestandsprojekt einen getrennten Source-Folder einzurichten, in dem sich ausschließlich blitzsaubere Referenzimplementierungen mit minimalem Umfang für alle Bausteine der Architektur befinden, die als verbindliche Muster fungieren. Wie sieht eine Entity aus? Wie sieht ein Testdaten-Builder für eine Entity aus? Wie ist ein Repository mit Cache für datenbankbasierte Konfigurationsdaten aufgebaut? Die Codebausteine in diesem Folder dienen KI und menschlichen Entwicklern als garantiert unverschmutzte Vorbilder und landen nicht in der Auslieferung. Auch hier gilt das gleiche Prinzip: Design before Implementation.

Ein anderer Punkt ist die Minimierung des Kontexts, den die KI für eine Aufgabe berücksichtigen muss. Je kleiner, desto besser. Anforderungsseitig sind präzise Spezifikationen eine gute Voraussetzung. Codeseitig empfiehlt Simon Martinelli in einem kürzlich veröffentlichten Podcast zu Spec-driven Development [17] den Architekturansatz der Self-contained Systems [18]. Grundidee ist die Zerlegung eines größeren Softwaresystems in Säulen unabhängiger Subsysteme, ohne in das Extrem von Microservices abzugleiten. Jedes der Subsysteme erfüllt eine klar umrissene fachliche Aufgabe und verfügt über eigene Datenhaltung und eigenes UI.

„Self-contained“ heißt: Die Subsysteme sind in sich abgeschlossen und brauchen nur minimalen Kontakt zueinander. Ein System-Use-Case bezieht sich auf Funktionalität innerhalb eines dieser Subsysteme, was dann auch für dessen Spezifikationen gilt. Eine KI, die auf Grundlage solcher Spezifikationen Code generiert, muss sich also nur auf das jeweilige Subsystem konzentrieren. Das kann den codeseitig zu berücksichtigenden Kontext erheblich reduzieren und umgekehrt die Wahrscheinlichkeit erhöhen, dass die KI brauchbaren Code in akzeptabler Zeit produziert.

Subsysteme in einer gemeinsamen Codebasis erfordern dann strenge Modularisierung und u. U. mehr Arbeit beim Prompting, damit der KI-Agent nicht aus Versehen links und rechts schaut. Mit separaten Codebasen für die Subsysteme geht man auf Nummer sicher, was aber wiederum viele andere Komplikationen mit sich bringen kann. Das ist Abwägungssache.

Eine sauber gegliederte Spezifikation hat den Vorteil, dass man einer KI auch klar abgegrenzte Teilaufgaben geben kann. Ist das Coden eines komplette Use Case ein zu dickes Brett, kann der Auftrag auch lauten: Implementiere Schritt 3.4.2 der Spezifikation. Es entsteht dann auch nicht so viel Code auf einmal und man tut sich leichter mit der Review. Man führt die KI sozusagen an ganz kurzer Leine. „Vise Coding“ statt „Vibe Coding“ nennt David Farago dieses Konzept, das sich mit Entwürfen der hier beschriebenen Detaillierung gut verbinden lässt [19].

Der statische Teil einer Spezifikation

Das oben verwendete Beispiel wurde bewusst so gewählt, dass es auch ohne Modellierung von Geschäftsobjekten einigermaßen verständlich ist. In der echten Praxis machen die Formulierungen der Abläufe aber intensiven Gebrauch von einem projektspezifischen Begriffsvokabular. Wenn der Leser einer Ablaufbeschreibung nicht zufällig alle darin auftauchenden Geschäftsobjekte und ihr Beziehungsgeflecht im Kopf hat, muss er sich das irgendwo anschauen können – und zwar nicht im Code, der nur Entwicklern zugänglich ist.

Ist die Formulierung „Wenn der Kunde einen Rabattcoupon eingelöst hat …“ überhaupt valide? Kann man irgendwo sehen, was ein Kunde und was ein Rabattcoupon ist und wie die Beziehung zwischen beiden aussieht? Vielleicht besteht die nur indirekt über den Umweg der Artikel einer Bestellung, und man sollte den Ablauf präziser formulieren – für Mensch und KI. Wenn diese Dinge nicht einsehbar sind, ist die Ablaufbeschreibung womöglich nur Kauderwelsch.

Abb. 6: Geschäftsobjekte als „umweltverträgliches“ UML-Klassendiagramm

Der Anspruch, dass die Entwürfe mit allen Beteiligten abgestimmt sind und sich permanent weiterentwickeln, gilt auch für die statischen Bestandteile. In den Projekten, in denen Specman und Word für die Ablaufbeschreibungen verwendet werden, kommen UML und im Besonderen Klassendiagramme zum Einsatz. Das ist trotz fehlender Änderungsverfolgung usw. ein akzeptabler Kompromiss, denn die Geschäftsobjekte ändern sich weniger häufig und enthalten auch nicht so viele Details wie die Abläufe. In Abbildung 6 ist das beispielhaft für die Geschäftsobjekte des Chatsystems dargestellt, auf die sich die Anforderung mit der Altersdarstellung bezieht. Allerdings sollte es schon „echtes“ UML sein und nicht nur gemalte Diagramme in DrawIO oder Visio. Der Hauptgrund ist die Unterscheidung zwischen dem Modell einerseits und dem Diagramm als eingeschränkte Sicht auf das Modell andererseits.

In größeren Softwaresystemen ist es wenig hilfreich, ein Riesentapetendiagramm mit allen Geschäftsobjekten darauf herzustellen. Sinnvoll und naheliegend ist eher, dass man Use-Case-bezogene Diagramme herstellt, die den Ablaufspezifikationen der Use Cases beigestellt werden (Abb. 3). Zentrale Geschäftsobjekte tauchen dann in diversen Diagrammen auf, sollen aber natürlich nur einmalig modelliert werden, sofern hier nicht nach dem Prinzip der Bounded Contexts bewusst Redundanzen erwünscht sind [20]. Diese Trennung von Modell und Diagramm ist eine wichtige Eigenschaft professioneller UML-Tools wie z. B. Enterprise Architect oder Visual Paradigm.

Hinzu kommt in diesen Tools die Möglichkeit des Aufbaus sogenannter UML-Profile, die u. a. einen Kanon projektspezifischer Stereotype und zugehöriger Tagged Values bereitstellen. Stereotype sind eine Ausdrucksform querschnittlicher Konzepte, über die sich damit verbundene Muster im Code adressieren lassen. Ein KI-Assistent kann über die Stereotype also auf die richtigen Patterns zur Codegenerierung hingewiesen werden, wie Abbildung 6 verdeutlicht. Aus einer View-Klasse sind völlig andere Dinge zu generieren als aus einer Entity-Klasse, wobei Letztere noch mit einem Tagged Value für den Tabellennamen angereichert ist. Mit einem guten Farbschema, unterstützenden Icons und möglichst fachlicher Terminologie lassen sich auch für die Anforderungsseite ansprechende Diagramme herstellen.

Use-Case-bezogene Diagramme haben übrigens den Nebeneffekt eines geschenkten Dependency-Trackings. Ändert man für einen Use Case etwas an der Struktur eines zentralen Geschäftsobjekts, lässt sich im Tool feststellen, in welchen Diagrammen und damit in welchen anderen Use Cases das Objekt eine Rolle spielt und wo man mögliche Auswirkungen prüfen muss.

Alle professionellen UML-Tools bieten Exporte in maschinenlesbare Formate an, z. B. XMI. Damit ist sichergestellt, dass auch KI die Modelle leicht versteht. Noch einfacher ist es mit Tools, die selbst auf Plaintext arbeiten und die grafischen Darstellungen generieren. Weit verbreitet ist z. B. PlantUML [21]. Leider ist hier die Trennung Modell/Diagramm nicht gegeben, was den Einsatz für größere Softwaresysteme erschwert.

Es ist auch die Frage, ob man mit der Optik generierter Diagramme klarkommt. Einer agentischen Codegenerierung ist Optik egal, solange die modellierten Strukturen präzise sind, aber wie bei den Abläufen sind die Adressaten für die Diagramme auch die Fachanforderer. Sinnvolle Platzierung nach fachlichen Gesichtspunkten, saubere Linienführung und Aufgeräumtheit sind hilfreich für das Verständnis. Auch bei den statischen Modellbestandteilen kann man sich nicht immer aussuchen, wie kompliziert die darzustellenden Zusammenhänge sind. Wo immer es geht, hält man die Dinge klein, etwa nach der C4-Methode [22] oder nach den Gestaltungskonzepten von Jacqui Read [23]. Wenn das aber einmal nicht geht, ist es ärgerlich, wenn die Einflussmöglichkeiten auf das Layout beschränkt sind. Man denke an die Analogie zur Bauzeichnung: Detailreichtum und Lesbarkeit sollten kein Widerspruch sein.

Spezifikation einführen

Wenn man Spezifikationen in einem Projekt einführen will, dann fängt man am besten klein an. Ähnlich wie bei der Einführung von Testautomatisierung muss man dabei gegen das kontraintuitive Gefühl ankämpfen, mehr Dinge herzustellen zu müssen, um am Ende effizienter zu sein. Dass ein Drittel mehr Code für Unit-Tests schlussendlich spart statt kostet, musste man auch erst mal verinnerlichen. Erfahrungsgemäß tun sich Youngster damit leichter als alte Hasen.

Für den Einstieg entwirft man z. B. für die nächste etwas kompliziertere Aufgabe einfach mal vorher ein Aktogramm, um darauf basierend zu coden. Das muss nicht gleich perfekt und mit den Anforderern abgestimmt sein. Erst mal für sich selbst üben, um z. B. das Arbeiten im Flow anzustreben [24]. Ist der Feinentwurf nämlich vorweg entstanden, kann man sich hinterher unterbrechungsarm dem Coden widmen. Wenn mir das als Mensch gelingt, sind die Voraussetzungen für KI-Unterstützung gut.

Ein anderer bewährter Einstieg ist z. B. eine fragmentarische Beschreibung aus einem Reverse Engineering von Code, dessen unklare Funktionsweise wiederholt Ärger macht – übrigens auch eine Tätigkeit, bei der KI helfen kann. Der Effekt, dass alle Beteiligten plötzlich in natürlicher Sprache nachlesen können, wie das System an der fraglichen Stelle arbeitet, ist ein Augenöffner.

Vollständig in das Software-Engineering integrierte Spezifikationen nehmen in der oben dargestellten Form etwa ein Fünftel der Zeit ein, die das Implementieren benötigt. Das ist ein grober Erfahrungswert über 15 Jahre und diverse Projekte in verschiedenen Firmen hinweg. Dabei entsteht ein Shift-Left-Effekt [25], der die Aufwände in teureren Folgephasen des Entwicklungsprozesses deutlicher reduziert als neuer Aufwand in den frühen Phasen entsteht. Es braucht nur seine Zeit, bis es einem in die DNA übergeht.

Immer erst die Spezifikation aktualisieren, bevor man an den Code geht. Das muss sich so natürlich und indiskutabel anfühlen wie: Immer erst den Gurt anlegen, bevor man losfährt. Dann funktioniert es.

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Fazit

KI-Unterstützung in der Softwareentwicklung hat, der besonderen Natur von LLMs wegen, ihre Tücken. Wo sie einerseits verspricht, das Coding zu beschleunigen, deckt sie andererseits gnadenlos auf, wo in Design und Code bisher „herumgesumpft“ wurde. Detaillierte, abgestimmte Spezifikationen und saubere Codestrukturen sind die Leitplanken, die einer KI den Weg weisen, um sie auch in anspruchsvollen Projekten für mehr als eine bessere Internetsuche zu verwenden. Insofern ist der Zeitpunkt gerade günstig, sich auf solide Entwurfsarbeit zu besinnen. Neue Spezifikationstools sind am Start und die Investitionsbereitschaft für alles rund um KI-Unterstützung ist hoch. Das Prinzip „Design before Implementation“ hat sich über mindestens 30 Jahre als zeitlos erwiesen und es ist unwahrscheinlich, dass KI das ändert, wenn selbst Vibe Coder es erkennen.

Ist dieser Artikel eigentlich mit KI-Unterstützung entstanden? In der Tat ist er das, und zwar genau nach den Prinzipien, die hier für Software beschrieben sind. Der Entwurf des Artikels ist up-front aus menschlicher Arbeit und vielen, vielen Diskussionen entstanden. Aber wenn es um die „Implementierung“ ging, konnte die KI dabei helfen z. B. einen angefangenen Satz sauber formuliert zu Ende zu bringen.

The post AI braucht Design: Die Rolle von Spezifikationen in Enterprise AI appeared first on JAX.

]]>
Golden Master Testing für Legacy-Systeme https://jax.de/blog/golden-master-testing-fuer-legacy-systeme/ Fri, 13 Mar 2026 16:00:34 +0000 https://jax.de/?p=209952 In diesem Artikel stellt der Autor mit dem Golden Master Testing eine Testmethode für die Migration von Legacy-Systemen vor. Anstatt zu versuchen, unbekannte Fachlichkeit nachträglich zu spezifizieren, wird das bestehende Verhalten des Systems systematisch beobachtet und abgesichert. So entsteht ein belastbares Sicherheitsnetz, das Veränderungen ermöglicht, ohne vollständiges Fachwissen vorauszusetzen. Gerade im Kontext von Migrationsprojekten entwickelt sich Golden Master Testing damit vom reinen Testwerkzeug zu einem zentralen Baustein für eine kontrollierte, schrittweise Modernisierung.

The post Golden Master Testing für Legacy-Systeme appeared first on JAX.

]]>
Die Migration von Legacy-Systemen scheitert selten an Technologie. Die eigentliche Herausforderung liegt fast immer woanders: in der fehlenden oder nur noch teilweise bekannten Fachlichkeit des bestehenden Systems. Über Jahre gewachsene Logik, implizite Geschäftsregeln und historische Sonderfälle sind oft ausschließlich im Code verborgen – die Dokumentation ist veraltet, Domänenexperten haben das Unternehmen verlassen oder sind in den Ruhestand gegangen und fachliche Tests decken nur geringe Teile ab, wenn sie überhaupt vorhanden sind.
Gleichzeitig besteht ein hoher Veränderungsdruck. Softwareversionen sind veraltet, Plattformen werden gekündigt und Bibliotheken nicht mehr mit Sicherheitsupdates versorgt. Das bestehende System muss also modernisiert, eventuell entkoppelt und/oder auf eine neue Plattform überführt werden. All das muss natürlich ablaufen, ohne den laufenden Geschäftsbetrieb zu gefährden.

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Die vorhandene Testabdeckung bietet in der Regel nicht genug Sicherheit, um die Migration durchzuführen. Die Testabdeckung zu erhöhen, erweist sich in der Regel als schwierig, weil klassische Testansätze in dieser Situation schnell an ihre Grenzen stoßen: Unit-Tests erfordern ein Verständnis der fachlichen Intention, Integrationstests setzen stabile Zustände der Umsysteme (inkl. der Datenbank) voraus und fachliche Abnahmetests benötigen explizite Erwartungen – all das fehlt häufig genau dort, wo Migration am dringendsten ist. In solchen Situationen ist Golden Master Testing für uns das Mittel der Wahl.

Golden Master Testing: die Grundidee

Die Grundidee dieses Ansatzes ist ebenso einfach wie wirkungsvoll: Statt das gewünschte Verhalten eines Systems zu beschreiben, wird das tatsächlich beobachtbare Verhalten als Referenz festgehalten.

Ein Golden Master ist dabei keine Spezifikation im klassischen Sinn, sondern quasi ein Snapshot des aktuellen Systemverhaltens. Für definierte Eingaben wird die erzeugte Ausgabe aufgezeichnet und als Vergleichsbasis für zukünftige Änderungen verwendet. Solange sich die Ausgaben nicht verändern, gilt das Verhalten als stabil – unabhängig davon, wie sich die interne Struktur des Systems entwickelt.

Dieser Ansatz ist besonders dort hilfreich, wo das fachliche Warum eines Systems nicht mehr vollständig bekannt ist. Golden-Master-Tests setzen bewusst außerhalb des Systems an. Sie betrachten es als Blackbox: Eingabe hinein, Ausgabe heraus. Welche Pfade im Code durchlaufen werden oder welche fachlichen Regeln dabei greifen, ist zunächst zweitrangig. Entscheidend ist allein, dass das beobachtete Verhalten reproduzierbar bleibt.

Wichtig ist dabei, zu verstehen, dass ein Golden Master kein Qualitätsurteil darstellt. Er konserviert bestehendes Verhalten – inklusive historisch gewachsener Sonderlogik, Inkonsistenzen oder fachlichen Fehlern. Golden Master Testing schafft somit zwar keine fachliche Klarheit, aber technische Sicherheit. Und diese ist häufig die notwendige Voraussetzung, um ein System überhaupt schrittweise zu modernisieren, zu refaktorisieren oder in eine neue Zielarchitektur zu überführen.

Damit verschiebt Golden Master Testing den Fokus von Spezifikation zu Stabilität: Erst wenn bekannt ist, was sich nicht verändern darf, wird es möglich, zu entscheiden, was sich tatsächlich verändern soll.

DIE KUNST DER SOTWARE-ARCHITEKTUR

Architecture & Design-Track entdecken

 

Abgrenzung zu anderen Testmethoden

Golden Master Testing wird häufig missverstanden – entweder als Ersatz für bestehende Testarten oder als spezielle Ausprägung von Integrationstests. Technisch ist das zwar richtig, organisatorisch nimmt der Ansatz jedoch eine eigene Rolle im Testportfolio ein und adressiert ein sehr spezifisches Problem: den fehlenden Zugang zur fachlichen Intention eines Systems.

Klassische Unit-Tests setzen voraus, dass die fachliche Bedeutung einzelner Codeeinheiten bekannt ist. Sie formulieren explizite Erwartungen an Methoden, Aggregates oder Services und prüfen diese isoliert. In Legacy-Systemen ist dieses Wissen jedoch oft nicht mehr vorhanden oder nur implizit im Code verankert. Golden-Master-Tests umgehen dieses Problem, indem sie nicht auf Codeeinheiten zielen, sondern auf beobachtbares Gesamtverhalten.

Integrations- und End-to-End-Tests prüfen das Zusammenspiel mehrerer Komponenten, also z. B. des Application Servers und der Datenbank, indem Requests über eine REST- oder (wahrscheinlich eher) SOAP-Schnittstelle abgesetzt werden. Solche Tests benötigen also klare Erwartungen, mit denen das tatsächliche Ergebnis verglichen werden kann. Golden-Master-Tests unterscheiden sich hier grundlegend: Sie bewerten nämlich gar nicht, ob ein Ergebnis korrekt ist, sondern lediglich, ob es sich im Vergleich zum bisherigen Verhalten verändert hat. Damit eignen sie sich besonders für instabile oder historisch gewachsene Systeme, die sich zwar technisch verändern lassen, deren Verhalten aber erhalten bleiben muss.

Häufig werden Golden-Master-Tests auch mit Approval-Tests gleichgesetzt. Auch hier gibt es tatsächlich technische Überschneidungen, da auch Letztere Ausgaben gegen gespeicherte Referenzen vergleichen. Der Unterschied liegt allerdings weniger im Werkzeug als im Ziel: Während Approval-Tests meist bewusst gestaltete, fachlich geprüfte Erwartungen absichern, dienen Golden-Master-Tests primär der Verhaltenskonservierung in Situationen fehlender Fachklarheit.

Im Unterschied zu klassischen Tests formulieren sie keine Erwartungen, sondern treffen eine Beobachtung. Sie beantworten nicht die Frage „Ist das Ergebnis fachlich korrekt?“, sondern „Liefert das neue System dasselbe Ergebnis wie das alte?“ Genau diese Verschiebung macht sie in Migrationsszenarien so wertvoll: Wo keine verlässliche Fachlichkeit verfügbar ist, kann zumindest fachliche Regression verhindert werden.

Entscheidend ist daher die richtige Einordnung: Golden Master Testing ist kein dauerhafter Ersatz für fachlich motivierte Tests. Vielmehr fungiert es als Einstiegspunkt. Es schafft die notwendige Sicherheit, um ein System schrittweise zu verändern, zu modularisieren oder zu migrieren. Erst auf dieser Grundlage können gezielt fachliche Tests ergänzt sowie veraltete Logiken hinterfragt und Domänenwissen wieder explizit gemacht werden. In einer nachhaltigen Teststrategie stehen Golden-Master-Tests somit nicht am Ende, sondern am Anfang.

Typische Einsatzszenarien

Nach dieser Beschreibung wird klar, dass Golden Master Testing seinen größten Nutzen in Situationen entfaltet, in denen Veränderung notwendig ist, das Risiko jedoch schwer abschätzbar bleibt. Besonders in Migrationsprojekten tritt diese Konstellation häufig auf.

Ein klassisches Einsatzszenario ist die technische Migration eines bestehenden Systems, etwa beim Wechsel der Plattform, der Programmiersprache oder der Laufzeitumgebung. Ob Hostsysteme, proprietäre Frameworks oder historisch gewachsene Eigenentwicklungen – häufig ist unklar, welche fachlichen Sonderfälle im Detail implementiert sind. Golden-Master-Tests ermöglichen es, das bestehende Verhalten vor der Migration zu erfassen und nach der Umsetzung gezielt zu vergleichen. Abweichungen werden sichtbar, ohne dass jede Regel explizit verstanden oder neu spezifiziert werden muss.

Auch beim schrittweisen Refactoring großer Legacy-Codebasen sind solche Tests ein bewährtes Mittel. Statt das System in einem großen Wurf umzubauen, können einzelne Module oder Komponenten isoliert verändert werden. Solange das beobachtete Verhalten stabil bleibt, lässt sich die interne Struktur kontinuierlich verbessern. Golden-Master-Tests sorgen dafür, dass strukturelle Änderungen möglich werden, ohne unbeabsichtigte fachliche Regressionen zu riskieren.

Ein weiteres häufiges Szenario ist die Extraktion von Modulen oder Services aus monolithischen Systemen. Bei der Abgrenzung fachlicher Verantwortlichkeiten ist das tatsächliche Verhalten oft aussagekräftiger als die vorhandene Dokumentation. Golden-Master-Tests helfen dabei, den bisherigen Funktionsumfang eines Moduls explizit festzuhalten und ihn beim Übergang in eine neue Architektur abzusichern.

Besonders relevant ist der Ansatz zudem bei Batchverarbeitung und datengetriebenen Prozessen. Hier sind fachliche Regeln oft über Jahrzehnte hinweg gewachsen und eng mit Datenformaten, Sonderfällen und historischen Annahmen verknüpft. Golden-Master-Tests ermöglichen es, diese Logik zunächst als Ganzes zu stabilisieren, bevor sie schrittweise analysiert, vereinfacht oder neu implementiert wird.
Gemeinsam ist all diesen Szenarien, dass das Testing nicht als langfristige Strategie verstanden wird, sondern als Enabler für kontrollierte Veränderung. Dort, wo vollständiges Verständnis fehlt, aber Verlässlichkeit erforderlich ist, bietet der Ansatz einen pragmatischen Weg, Migrationen schrittweise und mit kalkulierbarem Risiko umzusetzen.

Vorteile des Ansatzes

Der zentrale Vorteil des Golden Master Testings liegt in der frühen Herstellung von Sicherheit. In Situationen, in denen weder fachliche Spezifikationen noch belastbare Tests existieren, ermöglicht der Ansatz einen schnellen Einstieg in die Absicherung des Systemverhaltens. Damit wird Veränderung überhaupt erst verantwortbar.

Ein wesentlicher Nutzen besteht im geringen Bedarf an fachlichem Vorwissen. Golden-Master-Tests setzen nicht voraus, dass Geschäftsregeln vollständig verstanden oder neu modelliert werden. Stattdessen machen sie das bestehende Verhalten explizit und überprüfbar. Gerade in Migrationsprojekten, in denen Domänenexperten nur eingeschränkt verfügbar sind, ist das ein entscheidender Vorteil.

Darüber hinaus unterstützen die Tests eine inkrementelle Vorgehensweise. Veränderungen können schrittweise vorgenommen werden, etwa Modul für Modul oder Schnittstelle für Schnittstelle. Jede Anpassung lässt sich unmittelbar gegen das bisherige Verhalten prüfen. Abweichungen werden frühzeitig sichtbar, bevor sie sich in der Zielarchitektur verfestigen oder in spätere Projektphasen verschieben.
Ein weiterer Vorteil liegt in der Unabhängigkeit von internen Strukturen. Da Golden-Master-Tests auf beobachtbares Verhalten abzielen, bleiben sie auch dann gültig, wenn sich die interne Architektur grundlegend ändert. Daher sind sie besonders geeignet für tiefgreifende Refactorings, Plattformwechsel oder Sprachmigrationen.

Nicht zuletzt schaffen die Tests Transparenz über das tatsächliche Systemverhalten. Sie machen implizite Annahmen, Sonderfälle und historische Logik sichtbar, die zuvor im Code verborgen waren. Diese Transparenz bildet eine wichtige Grundlage, um in späteren Schritten fachliche Klarheit zu schaffen, Tests gezielt zu verfeinern und bewusste Entscheidungen über notwendige fachliche Änderungen zu treffen.

Zusammengefasst bietet Golden Master Testing keinen perfekten Testansatz, aber einen pragmatischen und wirkungsvollen Einstieg: Es reduziert Risiken, erhöht die Veränderungsgeschwindigkeit und schafft Vertrauen – genau dort, wo Migration sonst häufig ins Stocken gerät.

Grenzen und Risiken

So hilfreich Golden Master Testing in Migrationsprojekten ist, so wichtig ist ein realistischer Blick auf seine Grenzen. Der Ansatz schafft Sicherheit durch Stabilität – und genau darin liegt zugleich sein größtes Risiko.

Golden-Master-Tests sichern bestehendes Verhalten ab, unabhängig davon, ob dieses Verhalten fachlich korrekt, konsistent oder noch zeitgemäß ist. Historische Sonderfälle, implizite Workarounds oder längst überholte Geschäftsregeln werden mit konserviert. Wird dieses Verhalten ungeprüft in eine neue Zielarchitektur übernommen, so besteht die Gefahr, fachliche Altlasten zu zementieren, statt sie schrittweise abzubauen.

Ein weiteres Risiko liegt in der Fragilität der Tests. Viele Legacy-Systeme produzieren Ausgaben, die nicht vollständig deterministisch sind. Diese Aspekte müssen erkannt und aktiv angegangen werden. Ansonsten führen Golden-Master-Tests zu häufigen, wenig aussagekräftigen Abweichungen. In der Praxis untergräbt das schnell das Vertrauen in die Tests und damit ihren eigentlichen Zweck. Auf das Thema kommen wir weiter unten noch zurück.

Zudem liefern Golden-Master-Tests keine Erklärung für beobachtetes Verhalten. Sie zeigen, dass sich etwas verändert hat, nicht warum. Für tiefere fachliche Analysen, Fehlerursachen oder bewusste Weiterentwicklungen sind zusätzliche Schritte erforderlich. Ohne begleitende Refactorings, fachliche Klärung und gezielte Tests bleibt der Ansatz rein konservierend.

Schließlich besteht die Gefahr, Golden Master Testing als dauerhafte Lösung misszuverstehen. Wird der Ansatz nicht bewusst als Übergangswerkzeug eingesetzt, kann er zu einem Stillstand führen, bei dem Veränderung zwar technisch möglich, fachlich aber nie hinterfragt wird. Auf dieses Problem gehe ich im Abschnitt „Fallstricke und Antipatterns“ näher ein.

Golden-Master-Tests sind also kein Selbstzweck. Ihr Wert entsteht nur dann, wenn sie als temporäre Lösung verstanden werden – mit dem klaren Ziel, auf dieser Basis fachliches Verständnis aufzubauen, Tests zu verfeinern und das System langfristig weiterzuentwickeln.

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Einordnung in eine nachhaltige Teststrategie

Golden Master Testing entfaltet seine Stärke vor allem als Einstieg in unsicheres Terrain. In einer nachhaltigen Teststrategie nimmt der Ansatz daher eine klar begrenzte, aber zentrale Rolle ein: Er schafft Stabilität dort, wo Verständnis fehlt, und ermöglicht so die nächsten Schritte.

Zu Beginn einer Migration oder Modernisierung dient der Golden Master als Sicherheitsnetz. Er schützt vor unbeabsichtigten fachlichen Regressionen, während technische Strukturen verändert, Abhängigkeiten reduziert oder Module neu zugeschnitten werden. In dieser Phase steht nicht fachliche Korrektheit im Vordergrund, sondern die Gewissheit, dass sich das beobachtete Verhalten nicht unbemerkt verändert.

Mit zunehmendem Verständnis des Systems sollte sich der Schwerpunkt jedoch verschieben. Einzelne fachliche Bereiche werden expliziter, implizite Regeln werden sichtbar, und erste Domänenkonzepte lassen sich benennen. An diesem Punkt verlieren Golden-Master-Tests ihre Rolle als primäres Absicherungsinstrument und machen Platz für gezielte fachliche Tests: Unit-Tests auf Domänenebene, Integrationstests für klar definierte Schnittstellen und explizite Akzeptanzkriterien.

Wichtig ist dabei ein bewusster Umgang mit bestehenden Golden-Master-Tests. Sie können schrittweise ersetzt, reduziert oder auf grobe Regressionen beschränkt werden, während feinere fachliche Tests an ihre Stelle treten. Auf diese Weise wird das ursprünglich konservierte Verhalten nicht einfach eingefroren, sondern kontrolliert weiterentwickelt.

In einer langfristigen Perspektive sind Golden-Master-Tests somit kein Endzustand, sondern ein Katalysator für Lernprozesse. Sie ermöglichen es, Migrationen zu starten, bevor vollständiges Fachwissen verfügbar ist – und schaffen zugleich die Voraussetzung, dieses Wissen im Lauf des Projekts wieder aufzubauen und explizit zu machen.

Herausforderungen bei der technischen Umsetzung

Golden Master Testing bietet damit einen pragmatischen Ansatz, um Migration auch unter unsicheren fachlichen Rahmenbedingungen kontrollierbar zu machen. Indem das bestehende Verhalten systematisch beobachtet und abgesichert wird, entsteht die Sicherheit für tiefgreifende technische Veränderungen.

Die eigentliche Herausforderung beginnt jedoch dort, wo die Theorie in die Praxis überführt wird. Die Wirksamkeit von Golden-Master-Tests hängt maßgeblich von konkreten Entscheidungen ab: an welchen Stellen angesetzt wird, welche Szenarien abgesichert werden und wie mit instabilem oder historisch gewachsenem Verhalten umzugehen ist. Unbedachte Implementierungen führen schnell zu fragilen Tests, die mehr behindern als helfen.

Im nächsten Abschnitt widme ich mich daher der praktischen Umsetzung. Er zeigt, wie geeignete Einstiegspunkte identifiziert werden, wie aussagekräftige Testfälle ausgewählt werden und welche Gestaltungsprinzipien Golden-Master-Tests in Migrationsprojekten tatsächlich tragfähig machen.

Geeignete Einstiegspunkte finden

Der Erfolg der Methode hängt weniger von der Anzahl der Tests ab als von der Wahl der richtigen Einstiegspunkte. Nicht jeder Codebereich eignet sich gleichermaßen, um beobachtbares Verhalten stabil abzusichern. Gerade in Legacy-Systemen ist es entscheidend, dort anzusetzen, wo fachliche Wirkung entsteht.

Geeignete Einstiegspunkte sind in der Regel die öffentlich sichtbaren Schnittstellen. Dazu zählen externe APIs, Batchprogramme, Dateischnittstellen oder fachliche Services, die von anderen Systemen oder Prozessen genutzt werden. Aber auch ein eventuell vorhandenes UI ist ein guter Einstiegspunkt. Dort ist es erfahrungsgemäß allerdings aufwendiger, Tests zu realisieren, worauf ich im nächsten Abschnitt eingehe. Diese Schnittstellen bilden das tatsächliche Verhalten des Systems ab, das auch von Umsystemen und/oder Anwendern erwartet wird – unabhängig davon, wie komplex oder unübersichtlich die interne Implementierung ist.

Tief verschachtelte interne Methoden oder technische Hilfsfunktionen sind hingegen weniger geeignet. Sie besitzen oft keine eigenständige fachliche Bedeutung und ändern sich im Zuge von Refactorings zwangsläufig. Golden-Master-Tests auf dieser Ebene führen schnell zu instabilen Tests, die strukturelle Änderungen verhindern, statt sie abzusichern.

Bei der Auswahl von Einstiegspunkten helfen drei zentrale Kriterien: fachliche Relevanz, Stabilität und klar definierte Ein- und Ausgaben. Fachlich relevante Einstiegspunkte sind solche, deren Verhalten für nachgelagerte Systeme oder Geschäftsprozesse kritisch ist. Stabil sind Einstiegspunkte, deren Schnittstellen sich im Rahmen der Migration möglichst wenig ändern sollen. Klare Ein- und Ausgaben erleichtern schließlich die Vergleichbarkeit und reduzieren den Aufwand zur Stabilisierung der Tests.

In Migrationsprojekten empfiehlt es sich zudem, Einstiegspunkte entlang geplanter Migrationsschritte zu wählen. Wird ein Modul oder Service schrittweise ersetzt oder neu implementiert, sollte genau dieser Übergangspunkt durch Golden-Master-Tests abgesichert werden. Auf diese Weise entsteht ein direktes Feedback darüber, ob das neue System das bisherige Verhalten zuverlässig reproduziert.

Die bewusste Auswahl weniger und gut geeigneter Einstiegspunkte ist damit ein wesentlicher Erfolgsfaktor. Sie sorgt dafür, dass Golden-Master-Tests nicht zur Belastung werden, sondern gezielt dort Sicherheit schaffen, wo sie für die Migration den größten Nutzen haben.

Migration der Benutzeroberfläche

Die Migration der Benutzeroberfläche wird in Legacy-Projekten häufig unterschätzt, da in ihr oft implizite Fachlichkeit steckt. Bildschirmfolgen, Validierungen oder Navigationslogiken transportieren fachliches Verhalten, das selten dokumentiert ist. Gleichzeitig sind die UI-Technologien häufig veraltet und der Gedanke: „Wenn wir schon migrieren, dann auch das UI“ liegt nahe. Es ist allerdings davon abzuraten, Backend-Logik und UI gleichzeitig zu migrieren, weil die Absicherung durch Golden-Master-Tests dann besonders schwierig wird.

Ein Vorgehen, dass sich in unseren Projekten bewährt hat, besteht darin, die bestehende UI im ersten Schritt unverändert zu lassen und eine neue Service-Schicht, etwa in Form eines REST API, unter der Oberfläche einzuziehen. Um sicherzustellen, dass das fachlich korrekt geschieht, können UI-basierte Golden-Master-Tests von Beginn an eingesetzt werden. End-to-End-Tests mit Werkzeugen wie Playwright bedienen dann die bestehende Oberfläche automatisiert und überprüfen, ob sich das sichtbare Verhalten für die Nutzer trotz neuer Schnittstellen nicht verändert.

Sobald diese Absicherung etabliert ist, wird die Service-Schicht zum fachlichen Einstiegspunkt und kann selbst durch Golden-Master-Tests abgesichert werden. Erst danach wird das UI migriert oder neu implementiert – mit der Sicherheit, dass die Fachlichkeit durch die bestehenden Tests unverändert bleibt. So wird das UI schrittweise austauschbar, ohne implizite Fachlichkeit zu verlieren.

Testfälle auswählen – Qualität vor Quantität

Ein häufiger Fehler bei der Einführung von Golden-Master-Tests ist der Versuch, möglichst viele Testfälle zu erfassen. Gerade in Legacy-Systemen mit komplexem oder historisch gewachsenem Verhalten führt dieser Ansatz jedoch schnell zu hohem Pflegeaufwand und geringer Aussagekraft. Entscheidend ist nicht die Menge der Tests, sondern wie repräsentativ sie sind.

Ziel der Testfallauswahl ist es, typische und kritische Verhaltensmuster des Systems sichtbar zu machen. Bewährt hat sich eine bewusste Mischung aus Normalfällen, Grenzfällen und historisch relevanten Sonderfällen. Normalfälle bilden den alltäglichen Betrieb ab und sorgen dafür, dass zentrale Geschäftsprozesse stabil bleiben. Grenzfälle decken Eingaben an fachlichen oder technischen Grenzen ab und helfen, unerwartete Regressionen frühzeitig zu erkennen. Sonderfälle schließlich spiegeln häufig genau jene implizite Logik wider, die über Jahre hinweg in das System eingebaut wurde und bei Migrationen besonders risikobehaftet ist.

Bei der Auswahl von Testfällen ist zudem Zurückhaltung geboten. Jeder Golden-Master-Test konserviert Verhalten – und damit auch Komplexität. Tests sollten daher nur dort entstehen, wo ihr Schutz tatsächlich benötigt wird. Ein kleiner, gut gewählter Satz an Testfällen ist in der Regel wertvoller als eine breite, aber unstrukturierte Abdeckung.

Produktionsdaten können bei der Testfallerstellung eine wertvolle Grundlage sein, erfordern jedoch besondere Sorgfalt. Sie sollten anonymisiert, reproduzierbar und fachlich nachvollziehbar sein. Eine unreflektierte Übernahme großer Datenmengen führt häufig zu schwer wartbaren Tests, deren Aussagekraft mit der Zeit abnimmt.

Letztlich ist die Auswahl von Golden-Master-Testfällen immer eine Risikoentscheidung. Sie orientiert sich nicht an theoretischer Vollständigkeit, sondern an der Frage, welches Verhalten bei der Migration auf keinen Fall unbeabsichtigt verändert werden darf. Diese bewusste Fokussierung macht Golden Master Testing praktikabel und verhindert, dass der Ansatz selbst zum Hindernis für Veränderung wird.

Umgang mit nichtdeterministischem Verhalten

Golden-Master-Tests setzen voraus, dass identische Eingaben zu vergleichbaren Ausgaben führen. In der Praxis ist diese Annahme bei Legacy-Systemen jedoch häufig nicht erfüllt. Zeitabhängige Werte, zufällige Identifikatoren, implizite Sortierungen, externe Datenbanken oder andere externe Systeme führen dazu, dass sich Ausgaben trotz unverändertem fachlichem Verhalten unterscheiden. Ohne bewussten Umgang mit dieser Nichtdeterministik verlieren Golden-Master-Tests schnell ihre Aussagekraft.

Typische Ursachen nichtdeterministischen Verhaltens sind Zeitstempel, fortlaufende Nummern, technische IDs oder kontextabhängige Reihenfolgen in Listen und Reports. Auch externe Abhängigkeiten, etwa Datenbankzustände oder angebundene Systeme, können zu Abweichungen führen, die fachlich irrelevant, für den Testvergleich aber kritisch sind.

Der erste Schritt besteht darin, solche instabilen Anteile gezielt zu identifizieren. Abweichungen sollten nicht reflexartig akzeptiert oder ignoriert werden, sondern Anlass zur Analyse sein: Welche Teile der Ausgabe variieren und warum? Diese Transparenz ist Voraussetzung für jede weitere Stabilisierung.

Im zweiten Schritt werden die betroffenen Ausgabebereiche normalisiert. Zeitwerte können beispielsweise auf feste Referenzen gesetzt, technische IDs maskiert oder nicht relevante Felder aus dem Vergleich ausgeschlossen werden. Wichtig ist dabei, bewusst zwischen fachlich relevanten und rein technischen Unterschieden zu differenzieren. Alles, was fachliche Bedeutung hat, sollte weiterhin Bestandteil des Vergleichs bleiben.

Nicht jede Quelle von Nichtdeterminismus lässt sich sinnvoll beherrschen. In solchen Fällen ist zu prüfen, ob der gewählte Einstiegspunkt überhaupt für Golden-Master-Tests geeignet ist oder ob der Vergleich auf eine gröbere Ebene gehoben werden sollte. Manchmal ist ein weniger detaillierter, aber stabiler Vergleich wertvoller als ein vollständiger, aber fragiler Golden Master.
Der Umgang mit nichtdeterministischem Verhalten erfordert Disziplin und klare Entscheidungen. Wird diese Arbeit frühzeitig investiert, entstehen belastbare Tests, die strukturelle Veränderungen absichern, ohne bei jeder Ausführung Fehlalarme zu produzieren. Vernachlässigt man diesen Schritt, wird Golden Master Testing schnell als unzuverlässig wahrgenommen – und damit seines eigentlichen Nutzens beraubt.

DIE KUNST DER SOTWARE-ARCHITEKTUR

Architecture & Design-Track entdecken

 

Typische Fallstricke und Antipatterns

Golden Master Testing ist in Migrationsprojekten ein äußerst wirkungsvolles Instrument – zugleich aber auch anfällig für Fehlanwendungen. Viele der Probleme entstehen nicht durch das Konzept selbst, sondern durch falsche Erwartungen und mangelnde Disziplin im Umgang mit den Tests.

Ein verbreiteter Irrtum besteht darin, Golden-Master-Tests als Ersatz für fachliche Tests zu betrachten. Sie sichern ausschließlich das beobachtbare Verhalten eines Systems ab, nicht jedoch dessen fachliche Bedeutung oder Korrektheit. Wird dieser Unterschied nicht bewusst gemacht, bleibt Fachlichkeit dauerhaft implizit. Das Team vergleicht nur noch mit dem Vergangenen, ohne das Verhalten fachlich zu hinterfragen oder weiterzuentwickeln. Die Migration wird dadurch zwar technisch abgesichert, fachlich jedoch nicht beherrschbar.

Ein weiteres häufiges Problem ist die unkontrollierte Ausweitung der Golden-Master-Test-Suite. Es bringt nichts, automatisiert alle möglichen Varianten von Eingaben zu generieren und sie gegen das System zu schicken. In solchen automatisierten Set-ups entstehen schnell sehr große Mengen an Tests, deren Pflegeaufwand und Laufzeit stetig wachsen. Die eigentliche Aussagekraft der Tests leidet darunter, weil Abweichungen immer schwerer zu interpretieren sind. Statt gezielt Sicherheit zu schaffen, erzeugen solche Testlandschaften Unsicherheit und führen dazu, dass Warnungen ignoriert oder pauschal akzeptiert werden.

Besonders kritisch ist der bereits erwähnte Umgang mit nichtdeterministischem Verhalten. Dieses Verhalten darf nicht toleriert werden. Es muss sichergestellt sein, dass die Tests immer grün sind, wenn sich nichts Fachliches verändert hat. Ansonsten gehen echte fachliche Abweichungen im ständigen Rauschen unter. Ein instabiler Golden Master ist gefährlicher als gar keiner, da er vermeintliche Sicherheit vorgaukelt, wo keine existiert.

Eng damit verbunden ist das blinde Akzeptieren von Abweichungen. Werden Referenzausgaben aktualisiert, ohne die Ursache der Differenz verstanden zu haben, wird Wissen dauerhaft vernichtet. Fachliche Änderungen, Regressionen oder unbeabsichtigte Effekte werden stillschweigend in den Golden Master übernommen und damit legitimiert. Der Test dokumentiert dann nicht mehr das Verhalten des Legacy-Systems, sondern lediglich den aktuellen Zustand – unabhängig davon, ob dieser fachlich korrekt ist.

Auch die Wahl der Vergleichsebene ist ein häufiger Stolperstein. Zu feingranulare Vergleiche auf rein technischer Ebene, etwa vollständige Datenbank-Dumps oder detaillierte interne Strukturen, reagieren extrem empfindlich auf Änderungen in der Implementierung. Der fachliche Effekt geht dabei verloren, während der Wartungsaufwand steigt. Golden-Master-Tests sollten sich daher an der fachlichen Wirkung orientieren, nicht an technischen Details.

Das vielleicht subtilste Antipattern ist das dauerhafte Festhalten an diesen Tests. Was ursprünglich als temporäre Lösung für eine regressionsfreie Migration gedacht war, wird aus Bequemlichkeit, Unsicherheit oder aus Kostengründen zur Dauerlösung. In solchen Fällen bleibt Fachlichkeit dauerhaft implizit, fachliche Tests werden nie aufgebaut und die Migration bleibt unvollständig. Der eigentliche Erfolg von Golden Master Testing liegt darin, schrittweise überflüssig zu werden, sobald Fachlichkeit verstanden, modelliert und explizit getestet ist.

Richtig eingesetzt schaffen Golden-Master-Tests Vertrauen in Veränderungen bei unbekannter Fachlichkeit. Falsch eingesetzt konservieren sie genau die Intransparenz, die eine Migration eigentlich überwinden soll.

KI-Ensatz zur Generierung von Golden-Master-Tests

Der initiale Aufbau von Golden-Master-Tests ist in Migrationsprojekten oft der größte Hemmschuh. Gerade bei umfangreichen Legacy-Systemen fehlt es an Dokumentation, fachlichem Wissen und klaren Einstiegspunkten. Hier kann der gezielte Einsatz von KI einen entscheidenden Unterschied machen.

KI eignet sich besonders gut, um bestehende Legacy-Artefakte systematisch auszuwerten. Quellcode, Schnittstellenbeschreibungen, Datenformate oder bestehende Batchjobs enthalten implizites Wissen darüber, wie das System genutzt wird und welche Eingaben zu welchen Ausgaben führen. KI-gestützte Analysen können diese Informationen bündeln und daraus Vorschläge für sinnvolle Testfälle ableiten. Statt Tests manuell aus fachlichen Annahmen zu entwerfen, entstehen Golden-Master-Tests direkt aus dem tatsächlich beobachtbaren Verhalten des Systems.

Ein zentraler Mehrwert liegt dabei in der Skalierung. Während manuell erstellte Golden-Master-Tests meist auf wenige exemplarische Fälle beschränkt bleiben, kann KI eine große Bandbreite realistischer Szenarien abdecken. Insbesondere in Kombination mit Tools zur Messung der Testabdeckung, etwa JaCoCo, deren Ergebnisse als Input für KI dienen, kann sie automatisch typische Eingabekombinationen, Randfälle und Sonderlogiken auf Basis der Codeverzweigungen identifizieren und gezielt Tests generieren. So lassen sich auch selten genutzte, aber potenziell geschäftskritische Ablaufpfade absichern.
Wichtig ist jedoch, KI nicht als autonomes Werkzeug zu verstehen, das dauerhaft Tests schreibt. Die generierten Tests liefern eine gute Basis für das initiale Erstellen der Golden-Master-Tests. Sobald es in die fachliche Bewertung geht, ist das Team wieder gefordert. Gerade bei Abweichungen zwischen Legacy- und Zielsystem ist menschliche Interpretation unverzichtbar, um zu entscheiden, ob es sich um fachlich relevante Unterschiede oder um technische Artefakte handelt.

Richtig eingesetzt beschleunigt KI vor allem die frühe Phase der Migration. Sie reduziert den manuellen Aufwand, senkt die Einstiegshürde und macht Golden Master Testing auch bei sehr großen Systemen praktikabel. Gleichzeitig bleibt die Verantwortung klar beim Entwicklungsteam: KI generiert Tests, aber sie entscheidet nicht über fachliche Korrektheit.

Langfristig unterstützt KI auch den Übergang weg vom Golden Master. Erkenntnisse aus den generierten Tests lassen sich nutzen, um fachliche Regeln explizit zu formulieren und in intentionale Tests zu überführen. So wird KI nicht nur zum Beschleuniger der Absicherung, sondern auch zum Katalysator für das fachliche Verständnis des Legacy-Systems.

Der Einsatz von KI verändert damit nicht das Prinzip des Golden Master Testing, sondern dessen Wirtschaftlichkeit. Was früher Wochen oder Monate manueller Arbeit erforderte, kann heute in kurzer Zeit vorbereitet werden – ohne den Anspruch an fachliche Sorgfalt aufzugeben.

Nach den Tests ist vor der Migration – auch mit KI?

Sobald belastbare Golden-Master-Tests etabliert sind, verändert sich die Rolle von KI im Migrationsprojekt grundlegend. Während KI zuvor vor allem beim Aufbau der Tests unterstützt hat, kann sie nun direkt zur Umsetzung der Migration beitragen. Die Golden-Master-Tests bilden dabei den entscheidenden Sicherheitsanker: Sie definieren das erwartete Verhalten des Systems unabhängig von Technologie und Implementierung.

Auf dieser Grundlage kann KI gezielt eingesetzt werden, um Legacy-Code zu analysieren, zu transformieren oder neu zu strukturieren. Da das gewünschte Verhalten durch die Golden-Master-Tests bereits abgesichert ist, entsteht ein geschützter Raum für automatisierte oder teilautomatisierte Änderungen. KI-gestützte Refactorings, Übersetzungen zwischen Programmiersprachen und die Extraktion von Services können iterativ geschehen, ohne dass jede Änderung manuell fachlich verifiziert werden muss.

Ein wesentlicher Vorteil liegt in der Möglichkeit, alternative Implementierungen zu vergleichen. KI kann verschiedene Migrationsstrategien vorschlagen oder unterschiedliche Zielarchitekturen unterstützen, während die Golden-Master-Tests zuverlässig anzeigen, ob das fachliche Verhalten erhalten bleibt. Dadurch verschiebt sich der Fokus des Teams von der reinen Absicherung hin zur bewussten Gestaltung der Zielarchitektur.

Darüber hinaus lassen sich die Tests als Feedbackmechanismus für KI-gestützte Transformationen nutzen. Schlägt eine Migration fehl oder entstehen unerwartete Abweichungen, liefern sie präzise Hinweise darauf, an welchen Stellen fachliches Verhalten verändert wurde. Diese Rückkopplung ermöglicht es, KI iterativ zu steuern und schrittweise bessere Ergebnisse zu erzielen, statt große, riskante Migrationsschritte zu gehen.

Auch bei der Modularisierung bestehender Systeme entfaltet diese Kombination ihren Nutzen. KI kann dabei helfen, fachlich zusammengehörige Logik zu identifizieren, Abhängigkeiten sichtbar zu machen und Kandidaten für eigenständige Module oder Services vorzuschlagen. Die Golden-Master-Tests stellen sicher, dass diese Schnitte fachlich korrekt bleiben und nicht unbeabsichtigt Verhalten verlieren oder verändern.

Entscheidend ist dabei, dass KI nicht als autonomer Migrator verstanden wird. Sie arbeitet innerhalb eines klar definierten Rahmens, der durch Golden-Master-Tests und fachliche Entscheidungen des Teams vorgegeben ist. Die Verantwortung für Architektur, Fachlichkeit und Priorisierung bleibt weiterhin beim Menschen.

In dieser Rolle wird KI zu einem wirkungsvollen Verstärker: Sie beschleunigt Analyse und Umsetzung, während die Tests die notwendige Sicherheit liefern. Gemeinsam ermöglichen sie eine Migration, die schrittweise, kontrolliert und auch bei unbekannter Fachlichkeit verantwortbar bleibt.

Fachlichkeit zurückgewinnen statt nur bewahren

In vielen Legacy-Systemen ist Fachlichkeit nicht nur technisch verborgen, sondern auch organisatorisch entkoppelt. Wissen steckt in einzelnen Köpfen, in externen Dienstleistern oder in historisch gewachsenen Prozessen, die niemand mehr vollständig überblickt. Entscheidungen werden vermieden oder vertagt, weil niemand sicher sagen kann, welches Verhalten fachlich korrekt ist. Migration wird in solchen Situationen zwangsläufig defensiv: Ziel ist es, nichts kaputt zu machen – und nicht, das System aktiv weiterzuentwickeln.

Das Zusammenspiel aus Golden Master Testing und KI bietet hier eine Chance zur organisatorischen Neuausrichtung. Die Tests machen Systemverhalten unabhängig von Personen reproduzierbar. Sie entziehen implizitem Wissen seinen exklusiven Charakter und überführen es in ein gemeinsames, überprüfbares Artefakt. Fachliche Diskussionen stützen sich nicht länger auf Erinnerungen oder Vermutungen, sondern auf beobachtbares Verhalten.

KI verstärkt diesen Effekt, indem sie große Mengen an Verhalten systematisch erschließt und sichtbar macht. Nutzungsmuster, Sonderfälle und Abhängigkeiten werden transparent, ohne dass das Wissen einzelner Experten vorausgesetzt wird. Damit verändert sich die Gesprächsgrundlage mit der Fachabteilung: Statt „Wer weiß, wie das früher gemeint war?“, tritt die Frage „Wollen wir dieses Verhalten künftig noch?“ in den Vordergrund.

Dieser Perspektivwechsel ist organisatorisch entscheidend. Verantwortung für Fachlichkeit kann wieder im Team verankert werden, weil Entscheidungen auf einer belastbaren Basis getroffen werden. Golden-Master-Tests schaffen Sicherheit, KI reduziert Analyseaufwand – und gemeinsam ermöglichen sie es, fachliche Verantwortung bewusst zu übernehmen, statt sie weiter zu delegieren oder zu konservieren.

Mit fortschreitender Migration verschiebt sich die Rolle der Tests. Fachliche Regeln werden explizit formuliert, Golden-Master-Tests schrittweise abgelöst und Wissen in Modelle, Dokumentation und fachliche Tests überführt. Das System wird damit nicht nur technisch modernisiert, sondern organisatorisch neu verankert.

Migration wird so zu mehr als einem Umbau: Sie wird zu einem Prozess der Rückaneignung. Die Organisation gewinnt Entscheidungsfähigkeit zurück, Abhängigkeiten werden reduziert und Fachlichkeit wird wieder gestaltbar. Genau darin liegt der nachhaltige Wert dieses Vorgehens.

Stay tuned

Regelmäßig News zur Konferenz und der JAX-Community erhalten

[mc4wp-simple-turnstile]

 

Fazit: Sichere Migration beginnt mit beobachtbarem Verhalten

Migrationen scheitern selten an Technologie, sondern vielmehr an fehlender fachlicher Sicherheit. Legacy-Systeme enthalten oft jahrzehntelang gewachsene Fachlichkeit, die nur implizit im Code, in Datenstrukturen oder in Prozessen existiert. Hier setzt Golden-Master-Testing an: Es ermöglicht, bestehendes Verhalten zu konservieren, ohne es vollständig verstehen zu müssen, und schafft damit die Sicherheit für tiefgreifende Veränderungen.

Richtig eingesetzt sind diese Tests kein Ersatz für fachliche Tests, sondern ein Übergangswerkzeug. Sie machen implizites Wissen reproduzierbar und liefern die Grundlage für bewusste Entscheidungen. Abweichungen werden sichtbar, analysiert und fachlich eingeordnet. So entsteht die Möglichkeit, Fachlichkeit nicht nur zu bewahren, sondern aktiv zurückzugewinnen. Wissen, das zuvor in einzelnen Köpfen, alten Prozessen oder externen Dienstleistern verborgen war, wird ins Team zurückgeführt und kann bewusst gestaltet werden.

Der gezielte Einsatz von KI verstärkt diesen Ansatz erheblich. KI unterstützt beim Aufbau der Tests. Sie identifiziert Muster, macht Sonderfälle transparent und liefert Vorschläge für Refactoring oder Modularisierung – abgesichert durch die Golden-Master-Tests. Die Verantwortung für Fachlichkeit, Architektur und Priorisierung bleibt dabei beim Team, das nun auf einer stabilen und überprüfbaren Basis agiert.

Langfristig ermöglicht dieses Vorgehen eine Migration, die sowohl technisch als auch organisatorisch tragfähig ist. Golden-Master-Tests werden schrittweise durch explizite fachliche Tests ersetzt, Wissen wird dokumentiert und Fachlichkeit wieder gestaltbar. Migration wird so nicht nur zum Umbau von Systemen, sondern zum Prozess der Rückaneignung von Fachlichkeit und Verantwortung. Ihr größter Erfolg liegt darin, dass sie das Team befähigt, Entscheidungen bewusst zu treffen – sicher, nachvollziehbar und nachhaltig.

The post Golden Master Testing für Legacy-Systeme appeared first on JAX.

]]>