This blog post is also available in English
TL;DR
- KI verändert die Trade-offs der Softwareentwicklung, nicht das Ziel.
- Steckt die Produktivitätsgewinne der KI nicht nur in mehr Features.
- Investiert die gewonnene Zeit in Qualität, in den Abbau technischer Schulden und in wiederverwendbare Abstraktionen.
- Optimiert auf wartbare Software, nicht auf maximalen Durchsatz.
Mit dem Aufkommen der agentischen Programmierung haben wir bewiesen: Wir können Unmengen an Code in atemberaubendem Tempo produzieren. Aber was macht das mit der Wartbarkeit? Und wie können wir Coding Agents so einsetzen, dass sie nicht nur den Feature-Output erhöhen, sondern gleichzeitig auch die Qualität unserer Codebasis?
Wenn ich über diese Frage nachdenke, kommt mir immer wieder das Projektmanagement-Dreieck in den Sinn.
Die Idee hinter dem Projektmanagement-Dreieck (auch bekannt als eisernes oder magisches Dreieck) ist, dass zwischen Umfang, Kosten und Zeit reale Zwänge bestehen. In einem begrenzten Zeitraum kannst du entweder den Umfang der Features reduzieren oder das Budget erhöhen und zusätzliche Entwickler:innen einplanen, die an anderen Features arbeiten, die bis zum Termin fertig sein müssen. Wenn du darauf bestehst, Features auszurollen, ohne das Budget zu erhöhen oder den Umfang zu reduzieren, wirst du deinen Termin wahrscheinlich nicht einhalten können und mit ziemlicher Sicherheit die Codequalität insgesamt senken.
Über Jahrzehnte hinweg sind diese Zwänge des Projektmanagements ziemlich stabil geblieben, was uns eine gewisse Planbarkeit gegeben hat.
Einer der Gründe, warum sich KI so revolutionär anfühlt: Wir haben es jetzt mit einem völlig anderen Dreieck zu tun als gewohnt. Aus dem eisernen Dreieck ist ein neues, stählernes Dreieck mit anderen Eigenschaften geworden. Für Probleme, für die es bereits eine Lösung gibt, lässt sich nun ein Feature mit einem bestimmten Umfang in viel kürzerer Zeit und zu einem Bruchteil der Kosten entwickeln.
Umfang kürzen, um den Termin zu halten
Einen Termin nach hinten zu verschieben, löst einen Dominoeffekt aus: Andere Ziele geraten außer Reichweite. Und vor KI war es unberechenbar, das Problem mit Geld zu lösen, denn mehr Entwickler:innen bedeuten nicht automatisch mehr Durchsatz. Mein liebstes Mittel, um pünktlich zu liefern, war es deshalb immer, den Umfang zu kürzen.
Ein konkretes Feature lässt sich in Must-have- und Nice-to-have-Teilaufgaben zerlegen. Wenn wir diese Aufgaben in einem begrenzten Zeitraum umsetzen, sichern wir den Termin, indem wir nur die Must-haves umsetzen und vielleicht ein, zwei Nice-to-haves. Gegen Ende der Frist gehen wir häufig Kompromisse ein und nehmen bewusst technische Schulden in Kauf, um liefern zu können.
Diese Strategie gehört nach wie vor in den Werkzeugkasten. Aber mit KI haben wir jetzt andere Möglichkeiten, Software innerhalb unserer Zeitvorgaben zu liefern.
Mehr Features im selben Zeitraum liefern
Der naive Ansatz wäre, einfach zu versuchen, in derselben Zeit mehr Features zu liefern.
In unserem Beispiel: Wenn sich die Must-haves und Nice-to-haves aus unserem ursprünglichen Feature samt zugehöriger technischer Schulden in einem Bruchteil der Zeit liefern lassen, können wir vor dem Termin noch weitere Features samt zugehöriger technischer Schulden unterbringen.
Damit setzen wir KI ein, um den Feature-Output zu maximieren. Das kann durchaus ein sinnvoller Ansatz sein, gerade wenn viele Features anstehen und ein Termin drängt.
Das Problem daran: Wenn wir uns auf Feature-Durchsatz konzentrieren, ohne bewusst gegen technische Schulden zu arbeiten, häufen wir sie einfach schneller an.
Denk daran: Auch im echten Leben haben wir schon immer Kompromisse gemacht und (hoffentlich bewusst) technische Schulden aufgenommen, um pünktlich zu liefern.
Mit KI liefern wir schneller. Aber die KI wirkt auch wie ein Verstärker: Wir häufen dieselbe Menge technischer Schulden an wie ohne KI, nur eben in einem Bruchteil der Zeit. Gut möglich ist mit KI sogar, dass wir mehr technische Schulden anhäufen als sonst, denn die Modelle sind sehr gut darin, ausschweifende, überkonstruierte Lösungen zu produzieren, um die Aufgabe zu erledigen.
Das ist einer der Gründe, warum ich dem aktuellen Trend, Code zu mergen, ohne dass ein Mensch wirklich versteht, was dieser Code tut, skeptisch gegenüberstehe. Kurzfristig kann das zu Bugs und unerwünschtem Verhalten führen. Langfristig führt es außerdem dazu, dass sich überkomplizierter Code anhäuft.
LLMs sind Next-Token-Generatoren und als solche sind sie gut darin, Lösungen mit viel Code zu erzeugen. Weniger gut sind sie darin, sich kurz zu fassen.
Skeptisch bin ich auch bei einem Agenten, der in einer Schleife läuft, die Codebasis scannt und den Code aufräumt, den andere Agenten geschrieben haben. Denn es ist leichter zu beurteilen, ob man den Code überhaupt braucht, bevor er in der Codebasis landet. Ist er einmal gemergt, ahmen andere Agenten diesen suboptimalen Code mit hoher Wahrscheinlichkeit nach und der Ballast wird immer größer.
Über die Jahre habe ich ein bestimmtes Bauchgefühl entwickelt, wenn ich übermäßig komplizierten Code sehe. Eine Art Widerwillen. Manche würden sagen, wir müssten unsere Bauchreaktionen umtrainieren und Code akzeptieren, den wir sonst abgelehnt hätten, damit uns die Effizienzgewinne des Modells im Code Review nicht wieder verloren gehen.
Das hat mich zum Nachdenken gebracht: Warum habe ich dieses Bauchgefühl überhaupt? Was war eigentlich der Sinn von sauberem Code? Ich glaube nicht, dass sauberer Code je ein Selbstzweck war. Das Ziel war Software, die gut gewartet werden kann und leicht erweiterbar ist. Wie schaffen wir also genau das und nehmen trotzdem alle Vorteile der KI mit?
Mehr Zeit in ein Feature investieren, um die Qualität zu verbessern
Ein Ansatz: einen Tick langsamer machen und uns die Zeit nehmen, die wir brauchen, um das Feature wirklich rund zu machen, den Code aufzuräumen und es zu liefern, ohne Kompromisse, die die technischen Schulden in unserer Codebasis erhöhen.
Die Must-haves und Nice-to-haves aus unserer Aufgaben-Aufteilung lassen sich jetzt schnell genug umsetzen, um ein Feature vor dem Termin zu liefern, ohne weitere technische Schulden anzuhäufen.
Das verlangt einen Sinneswandel: weg vom reinen Output, hin zu Output mit Qualität. Selbst mit diesem Sinneswandel liefern wir oft immer noch früher, als wir es in der Zeit vor KI getan hätten. Nur investieren wir hier bewusst mehr Zeit ins Endprodukt, um die Codebasis nicht schlechter zu hinterlassen, als wir sie vorgefunden haben.
Und ja, ich würde das Feature trotzdem in kleinere Teilaufgaben zerlegen, um die Arbeitspakete klein genug für meinen mentalen Kontext zu halten.
Es besser hinterlassen, als du es vorgefunden hast
Der logische nächste Schritt wäre, bewusst Zeit einzuplanen, um unsere Codebasis nach und nach besser zu hinterlassen, als wir sie vorgefunden haben.
Damit meine ich nicht zwangsläufig, dass wir uns ins Projekt-Backlog stürzen und die verzwickten, komplizierten Refactoring-Tickets herausholen, um die wir bisher einen Bogen gemacht haben (auch wenn das definitiv eine Option ist).
Ich meine etwas Einfacheres: ganz am Anfang unseres Workflows bewusst Zeit einzuplanen, um einen Blick auf die Codebasis und auf den Code zu werfen, den wir ohnehin ändern müssen. Und dann zu schauen, ob es kleinere Aufgaben gibt, für die ich mir jetzt die Zeit nehme und die die Codebasis besser machen, wenn ich mit der eigentlichen Aufgabe fertig bin.
Zusätzlich zu den Must-haves und Nice-to-haves, die wir weiterhin umsetzen, planen wir ganz am Anfang unserer Aufgabe einen Zeitblock ein, um technische Schulden abzubauen. Unterm Strich liefern wir trotzdem alles lange vor dem Termin.
In der Praxis ist es oft meine eigene Ungeduld, sofort mit dem „echten Feature" anzufangen, die mir das schwer macht. Obwohl ich für ein Feature inzwischen viel weniger Zeit brauche, ist mir dieser Gedanke bei der Planung oft immer noch nicht präsent.
Wenn ich ein altes TODO im Code sehe, will ich es oft ignorieren, weil ich gerade an etwas anderem arbeite. Aber wenn ich mir die Zeit nehme, es mir anzusehen, ist es gar nicht schwer, ein bisschen Zeit einzuplanen, mich darum zu kümmern und die Codebasis Stück für Stück aufzuräumen.
Zeit in Libraries und wiederverwendbare Abstraktionen investieren
Neben der Zeit, die wir in das Aufräumen der Codebasis stecken, gibt es noch eine Option: Zeit in interne Libraries und wiederverwendbare Abstraktionen für unser System zu investieren, die uns das Umsetzen von Features in Zukunft erleichtern.
Nachdem wir den Abbau der technischen Schulden eingeplant haben, können wir einen Block Deep-Work-Zeit einplanen, um an internen Libraries und wiederverwendbaren Abstraktionen zu arbeiten, mit denen wir dann die Must-have- und Nice-to-have-Features in einem Bruchteil der Zeit fertigstellen. Die Zeit, die wir in die Libraries und Abstraktionen investieren, ist vielleicht höher, als die Aufgaben ohne sie umzusetzen. Trotzdem liegt die gesamte Entwicklungszeit für alle Aufgaben, Abstraktionen und den Abbau technischer Schulden wahrscheinlich immer noch locker innerhalb des Termins, den wir für das Feature ohne KI-Unterstützung angesetzt hätten.
Besonders nützlich ist das aus einem Grund: KI-Modelle produzieren oft Lösungen mit viel Boilerplate und blähen die Codebasis auf. Gleichzeitig sind sie aber richtig gut darin, eine Library zu verstehen und für etwas Ähnliches einzusetzen.
Das ist keine grundlegend neue Idee. Zu Beginn meines aktuellen Projekts habe ich Zeit investiert, um Form-Helpers für unsere Spring-Boot-Anwendungen zu bauen, also eine Abstraktion in unserer Pattern Library, die die Spring-Validierungsfehler automatisch mit unseren HTML-Templates verknüpft und dafür sorgt, dass Labels und Fehler barrierefrei gerendert werden. Diese Investition in unsere Codebasis hat zu deutlich weniger Boilerplate geführt, wenn es um Formulare geht.
Mithilfe von KI, um verschiedene Ansätze auszuprobieren, habe ich die Abstraktion kürzlich um ein Custom Element und einen API-Contract für ein Feld erweitert, mit dem wir ein Formularfeld schon vor dem Absenden live validieren können. Jetzt, wo wir eine Abstraktion gefunden haben, die uns gefällt, brauchen wir auch dafür in Zukunft keine Boilerplate mehr.
Ein weiteres Beispiel ist eine kleine Arbeit, die ich kürzlich gemacht habe: Custom Annotations für das Legacy-Berechtigungssystem, an das wir unser System anbinden müssen. Dadurch brauchen wir nur noch eine Zeile Code statt drei, und der Code ist deutlich lesbarer. Und weil wir das schon häufig verwendet haben, spart es mit der Zeit spürbar Boilerplate in der Codebasis.
Für solche Arbeit muss man sich wirklich eine ordentliche Timebox setzen. Früher haben wir Timeboxes genutzt, um den Umfang einzufangen, wenn er ausuferte. Mit dem Aufkommen von KI finde ich sie zusätzlich hilfreich als Erinnerung daran, dass gute Arbeit ihre Zeit braucht. Bei Deep-Work-Aufgaben war der Engpass immer die Geschwindigkeit, mit der ich denken kann, nicht die, mit der ich tippen kann. Daran hat sich nichts geändert, auch wenn ich Ideen jetzt viel schneller prototypen und wieder verwerfen kann.
Nach einer Phase konzentrierter Deep Work bin ich außerdem immer wieder beeindruckt vom Unterschied zwischen Deep Work (die sich so langsam anfühlt) und Routinearbeit (die sich mit KI fast augenblicklich anfühlt). Ich glaube, das ist eine Frage der Wahrnehmung. Auch wenn sich Deep Work langsam anfühlt, liefere ich damit meist immer noch mehr Qualität in weniger Zeit als vor KI. Und ich entwickle nebenbei Abstraktionen, die die Bausteine dafür liefern, in Zukunft besseren Code in höherem Tempo zu schreiben.