Ich muss zugeben, mich hat die Komplexität aktueller Open-Source-Projekte schon immer geärgert. Alle fangen sie klein an, finden pragmatische und einfache Lösungen für allerlei Probleme. Mit der Zeit jedoch wandern immer mehr Use-Cases in die Frameworks. Sie werden komplexer. Mehr Code benötigt aber auch immer mehr Wartung. Fixes in Komponenten, die man nie verwendet, zwingen zum Update. Von der umfangreichen Konfiguration mal ganz zu schweigen. Außerdem haben die Anbieter ein Interesse daran, dass ihr Framework und damit ihre Marke immer mehr Abnehmer findet. Das geht nur mit immer mehr Features, noch mehr Trainings, Blogposts und Vorträgen. Dies hat auch damit zu tun, dass es heute fast keine klassischen Open-Source-Projekte mehr gibt, die von privaten Enthusiasten geschrieben werden. Praktisch immer stecken Firmen mit entsprechenden kommerziellen Interessen dahinter, die eine immer größere Nutzerbasis fordern.

Ist das also richtig, was wir tun? Ist es gut, wenn wir in Wirklichkeit nur 10 % eines Codes tatsächlich nutzen? Wenn die 10 % die Probleme für mich lösen, ist das doch ok, werden einige sagen. Dennoch ist da Code, den wir nicht kennen und viel mehr Code als wir eigentlich brauchen. Da hilft es auch nicht, wenn wir in langen SBOMs (Software Bill of Materials) aufschreiben, welche Abhängigkeiten wir haben und welche CVEs (Common Vulnerabilities and Exposures) die haben. SBOMs sind statisch, CVEs nicht. Wenn ein neuer CVE bekannt wird, machen wir oft im besten Falle einfach ein Update. Wir ignorieren aber dabei, dass wir meistens nur bekannte Fehler durch unbekannte Fehler ersetzen. Ein gutes Gewissen gibt das aber natürlich trotzdem und die Compliance ist auch zufrieden.

Schätzungsweise 80 % des Codes, den wir in unseren Applikationen verwenden, stammen aus für uns schwer fassbaren Quellen. Wir haben uns an die einfachen Quick-Starts gewöhnt. Wir haben Programmiersprachen, Microservices und Modularisierung, tolle Dokumentationen, die Abstraktionen, die Entwicklungstools, die Patterns, den Clean Code, Software Craftsmanship erfunden, um verstehen zu können, was wir in welcher Reihenfolge und an welcher Stelle zu tun haben. Ohne das alles könnten wir nicht tun, was wir tun. Der Mensch braucht Abstraktionen, weil sein Geist nicht in der Lage ist, die gesamte Komplexität der Anforderungen und des Codes zu erfassen.

State of Play

Stellen wir uns eine schamlos simple Web-Application vor, die einen einfachen Text bei einem HTTP GET zurück liefert. Stellen wir uns weiter vor, wir realisieren dies auf 3 unterschiedlichen Wegen.

Weg 1: Spring Boot

Spring Boot ist bekannt, nehme ich an. In den meisten Unternehmen wird man auch noch selten Menschen finden, die mit dem JDK (Java Development Kit) alleine etwas anfangen können. Sie sind eher Spring Boot Entwickler als JDK Entwickler. Die Anforderung ist einfach und der zugehörige Code ebenso. Er würde ungefähr so aussehen:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}


package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String sayHello() {
        return "Hello from the example service!";
    }
}

Das sind wenige Zeilen Code, 13 sind es (ohne Imports). Trotzdem steckt dahinter eine ganze Menge. Die simple dazugehörige pom.xml lädt ca. 60 JARs herunter, die ca. 30 MB Platz belegen. Schaut man in die Quellen, die diese JARs erzeugen, kommt man auf ungefähr 1,8 Mio. Lines of Code. Dazu kommt noch das OpenJDK, dessen SDK aus ca. 10 Mio. Codezeilen erstellt wird. Wie viel davon wird nun von unserem kleinen Beispiel verwendet? Sicherlich weit weniger als 10 %.

Auch wenn wir wenig der 12 Mio. LoCs verwenden, hat der Code dennoch Fehler, sogenannte CVEs. Was das JDK angeht, kann man davon ausgehen, dass alle 3 Monate durchschnittlich 13 CVEs gefixt werden. Natürlich ist nur eine geringer Anteil davon wirklich kritisch für Produktionssysteme, wenn wir unseren kleinen Service ins Internet stellen würden. Betrachtet man Spring Boot, wurden im letzten Jahr (ungefähr März 2024 bis März 2025) durch Releases rund 4 signifikante CVEs behoben, darunter eine direkt in Spring-Boot und mehrere wichtige in Kernabhängigkeiten. Wenn man den Zeitraum leicht bis Ende 2023 erweitert, käme eine weitere hinzu.

Was bedeutet das nun? Man kann wahrscheinlich davon ausgehen, dass sich in jeder aktuellen Version >10 unbekannte CVEs befinden. Die Natur solcher CVEs ist es, dass sie dynamisch auftauchen, während unsere Software statisch ist. Aber das ist eine andere Geschichte.

Weg 2: JDK

Das JDK ist inzwischen weniger bekannt als Spring Boot.

/**
 * Minimal OpenJDK HTTP GET server. Responds to GET requests on '/'.
 */
import com.sun.net.httpserver.HttpServer;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;

public class SimpleServer {
    public static void main(String[] args) throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
        String response = "Hello!";
        byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8);

        server.createContext("/", exchange -> {
            exchange.sendResponseHeaders(200, responseBytes.length);
            try (OutputStream os = exchange.getResponseBody()) {
                os.write(responseBytes);
            }
            exchange.close(); 
        });

        server.start();
    }
}

15 Codezeilen sind es hier. Aber das ist natürlich nur die halbe Wahrheit. Im Spring-Beispiel ist natürlich eine Menge Errorhandling und viele andere Dinge mit dabei. Implementiere ich zumindest eine Fehlerbehandlung selbst, werde ich auf ungefähr 50 LoCs kommen. Aber auch das ist nur die halbe Wahrheit, denn ich habe hier 1,8 Mio. LoC weniger als im ersten Beispiel verwendet.

Weg 3: Ins Extreme

Niemand will noch Assembler programmieren. Der Code ist schlecht portabel, fehleranfällig und kein Compiler ist da, der Anweisungen automatisch optimiert. Aber was es bedeutet, alle Abstraktionen und Layer wegzulassen, ist doch interessant. Wie sähe unser kleiner Linux-Service denn aus?

So:

section .data

    ; A minimal HTTP response (HTTP 1.1, text/plain)
    http_response db  "HTTP/1.1 200 OK", 13, 10
                   db "Content-Type: text/plain", 13, 10
                   db "Content-Length: 31", 13, 10
                   db "Connection: close", 13, 10
                   db 13, 10
                   db "Hello from Assembler HTTP server!", 13, 10
    http_response_len equ $ - http_response

    ; sockaddr_in structure (IPv4) for 0.0.0.0:8080
    sockaddr_in db 2, 0         ; sin_family = 2 (AF_INET)
                dw 0x901f       ; sin_port = 0x1F90 (8080 in network byte order)
                dd 0            ; sin_addr = 0.0.0.0
                dq 0            ; sin_zero[8] = {0}

section .bss
    buffer resb 1024    ; buffer to read incoming request data

section .text
global _start

; -----------------------------------------------------------------------------------
; _start: Program entry point
; -----------------------------------------------------------------------------------
_start:
    ; 1) socket(AF_INET, SOCK_STREAM, 0)
    mov rax, 41                  ; sys_socket call number
    mov rdi, 2                   ; AF_INET = 2
    mov rsi, 1                   ; SOCK_STREAM = 1
    xor rdx, rdx                 ; protocol = 0
    syscall
    cmp rax, 0
    js  error                    ; if error, jump to error
    mov r12, rax                 ; store socket fd in r12

    ; 2) bind(sock, &sockaddr_in, sizeof(sockaddr_in))
    mov rax, 49                  ; sys_bind
    mov rdi, r12                 ; socket fd
    mov rsi, sockaddr_in
    mov rdx, 16                  ; sizeof(sockaddr_in) = 16
    syscall
    cmp rax, 0
    js  error

    ; 3) listen(sock, backlog=5)
    mov rax, 50                  ; sys_listen
    mov rdi, r12
    mov rsi, 5                   ; backlog
    syscall
    cmp rax, 0
    js  error

accept_loop:
    ; 4) accept(sock, NULL, NULL)
    mov rax, 43                  ; sys_accept
    mov rdi, r12
    xor rsi, rsi                 ; no sockaddr for client
    xor rdx, rdx                 ; no socklen_t pointer
    syscall
    cmp rax, 0
    js  error
    mov r13, rax                 ; store connected client fd in r13

    ; 5) read request from the client (not strictly necessary for a static response)
    mov rax, 0                   ; sys_read
    mov rdi, r13                 ; client fd
    mov rsi, buffer              ; read into 'buffer'
    mov rdx, 1024                ; max 1024 bytes
    syscall
    ; ignoring request details

    ; 6) write a simple response back
    mov rax, 1                   ; sys_write
    mov rdi, r13                 ; client fd
    mov rsi, http_response
    mov rdx, http_response_len
    syscall

    ; 7) close the client connection
    mov rax, 3                   ; sys_close
    mov rdi, r13
    syscall

    ; 8) loop to accept new connections
    jmp accept_loop

error:
    ; On error, exit with a status code of 1
    mov rax, 60  ; sys_exit
    mov rdi, 1
    syscall

Das Ergebnis hat ungefähr 70 LoC. Wenn man das kompiliert, belegt der Binary Server auf der Platte ganze 9.2 KB und Top bescheinigt ihm zur Laufzeit 0.0% Speichernutzung.

Klar, Fehlerbehandlung etc. fehlt hier - und auch sonst so einige Dinge. Dennoch, die Menge an zusätzlichem Code, den uns unsere Frameworks, Umgebungen, Layer etc. bringen und an die wir uns längst gewöhnt haben, ist schon erstaunlich. Unnötig zu erwähnen, dass dieser Assembler-Code tatsächlich mindestens rund 12 Mio. LoC weniger enthält als der erste hier gezeigte Weg.

Was folgt daraus?

Die Diagnose ist klar: Wir akzeptieren oft Code-Massen selbst für einfache Aufgaben – Millionen Zeilen und dutzende Abhängigkeiten für ein «Hello World», von denen wir nur einen Bruchteil nutzen. Eigentlich ist das ineffizient, mindestens zur Laufzeit - und riskant, was Fehler und Vulnerabilities angeht.

Warum das genau so notwendig ist, ist eindeutig. Menschen benötigen eben Abstraktionen, um Komplexität zu bewältigen, sich auf das Wesentliche konzentrieren zu können und ohne sich in Details zu verlieren. Es ist auch effizient, weil Menschen sich nicht ständig über Dinge absprechen müssen, die sie schon abgesprochen haben. Das erlaubt auch, Code zu verwenden, der bereits geschrieben wurde.

Architekturen sind Abstraktionen, um komplexe Systeme in handhabbare, voneinander entkoppelte Komponenten zu gliedern, deren Interaktionen klar definiert sind, was die Wartbarkeit, Erweiterbarkeit und das menschliche Verständnis des Gesamtsystems überhaupt erst möglich macht. Die geschaffenen und etablierten Lösungen dazu sind Dinge wie Microservices, Self-Contained Systems, Domain-Driven Design oder Frameworks wie Scrum. Mensch ist eben Mensch und hat Defizite.

Was ändert die KI?

Noch ist es eine Hypothese und es ist noch nicht so weit. Perspektivisch aber wird die KI weniger Probleme damit haben, komplexe Aufgaben und komplexen Code zu verstehen als der Mensch. Ebenso wird das reine Erstellen von Code, oder zumindest dessen Menge, wenig Einfluss auf die Gesamtkosten eines Projekts haben.

Die Folgen daraus sind spekulativ, zugegeben. Extrapoliert man aber die aktuellen Entwicklungen könnten sich folgende Dinge ändern:

Stellen wir uns vor, wir definieren nur noch präzise Anforderungen, und eine KI generiert den exakt passenden, minimalen Code. Kein Ballast, keine unnötigen Features, keine ungenutzten Abhängigkeiten. Dies könnte so weit gehen, dass bisherige Frameworks ihre Bedeutung verlieren. Beispielsweise ist das Konzept einer Persistenzschicht gängiges Wissen. Eine KI könnte genau die Anteile davon generieren, die die aktuelle Anwendung tatsächlich benötigt - immer wieder neu, nach jeder neuen Anforderung. Statt ein Framework zu verwenden, würde der Mensch zu einem Architekten der Anforderung und Validierer des Ergebnisses. Für Hersteller von Frameworks bedeutet dies wahrscheinlich, dass sie sich anpassen müssen. Sie werden andere innovative Wege finden müssen, um weiter zu existieren. Statt Code werden sie Pattern, Templates, Konzepte, Regeln, Prinzipien liefern müssen, die ein LLM versteht. Inwieweit dies aber noch ein kostbares Spezialwissen darstellt, wird sich noch erweisen müssen. Denn jede Expertise wird durch LLMs allgemein zugänglich und abfragbar.

Was bedeutet das für unsere bewährten Methoden und Prinzipien?

Zum Beispiel Architekturmuster wie Microservices oder Self-Contained Systems: Sind sie noch nötig, um Komplexität zu managen, wenn die KI atomare, perfekt zugeschnittene und bei Bedarf neu generierbare Einheiten erstellt? Ändert sich die Notwendigkeit für entkoppelte Deployments und Skalierungseinheiten? Welche Treiber von SW-Architekturen bleiben übrig? Oder gibt es neue?

Domain-Driven Design (DDD): Wenn die Übersetzung von Fachlichkeit in Code durch eine KI erfolgt, wie verändert sich der Wert aufwendiger Domänenmodellierung? Kann DDD als Vorlage für Anweisungen an die KI dienen oder wird es durch KI teilweise überflüssig?

Frameworks wie Scrum: Wenn die Code-Erstellung drastisch beschleunigt wird, wie wirkt sich das auf Iterationszyklen, Teamstrukturen und die Organisation von Entwicklungsprozessen aus? Verschiebt sich der Fokus von der Management-Komplexität der Softwareerstellung zur Komplexität der Problemdefinition für die KI?

Wie auch immer, die KI wird es erfordern, die Fundamente unserer heutigen Arbeitsweisen kritisch zu hinterfragen und uns auf eine Zukunft vorzubereiten, in der unsere Rollen und die Werkzeuge neu definiert werden.

Wir haben da schon einige Ideen. Stay tuned…