Wo wir stehen
Der erste Teil endete mit einem Modell und einem Vorbehalt. Wer ihn gelesen hat, kennt beides; wer hier einsteigt, dem sei die Ausgangslage in wenigen Sätzen wiedergegeben, denn ohne sie bleibt vieles im Folgenden bloße Bauanleitung.
Ausgangspunkt war eine flache Aufgabenliste in einem fremden Werkzeug, deren eigentliche Gliederung nicht im System stand, sondern in einer Namenskonvention. Aus ihr wurde eine vierstufige Ordnung von Inhalten, und aus einem einzigen, über die Jahre überladenen Statusfeld mit sechzehn Werten wurden drei voneinander unabhängige Dimensionen: die Akquise, die redaktionelle Arbeit und die Herstellung. Der Verlauf jeder dieser Dimensionen wird nicht überschrieben, sondern angehängt, geordnet nach einer aufsteigenden Folgenummer statt nach einem Zeitstempel. Auf dieser Grundlage entstanden ein fachliches Konzept, ein Katalog der Vorgänge und daraus sieben Ansichten, deren Entwurf am Ende als interaktives Modell vorlag.
Der Vorbehalt lautete, dass ein Modell, das sich nur gegen sich selbst behaupten muss, stets gewinnt. Dieser zweite Teil ist die Einlösung: Aus dem Entwurf wird eine Anwendung, durch die der wirkliche Bestand hindurchgeht.
Der technische Boden sei vorweg genannt, damit im Weiteren nicht darüber gerätselt werden muss. Die Anwendung ist durchgehend in Java geschrieben, die Oberfläche mit Vaadin, die Speicherung übernimmt EclipseStore auf einem Objektgraphen, das Ausliefern ein eingebetteter Jetty, die Berechtigungen ein schlankes eigenes Werkzeug. Weder ein Anwendungsserver noch ein umfassendes Rahmenwerk kommen zum Einsatz; wo das JDK genügt, genügt es. Auf eine Einführung in Vaadins Bausteine wird verzichtet — sie werden als bekannt vorausgesetzt, und interessant ist ohnehin nicht, welche Komponente etwas anzeigt, sondern an welchen Stellen die Bausteine nicht ausreichten.
Was folgt, ist deshalb kein Erfolgsbericht, sondern ein Werkstattbericht: der Aufbau in der Reihenfolge der Entwurfsvorgaben, die Übertragung des Modells in die Oberfläche, die beiden Ansichten ohne Fertigbaustein, die Brücke aus dem alten System, die Prüfung ohne Browser sowie am Ende eine Bilanz, die auch benennt, was misslang.
Der Aufbau in der Reihenfolge des Briefs
Zwischen dem Entwurf und der laufenden Anwendung liegt jene Strecke, über die in Beiträgen dieser Art meist geschwiegen wird – vermutlich, weil sie unspektakulär ist. Gerade sie entscheidet aber darüber, ob die eingangs behauptete Rechnung aufgeht. Die Umsetzung schloss unmittelbar an die View-Briefs an und umfasste insgesamt zweiundsiebzig Commits. Der Weg dorthin verlief in drei erkennbaren Phasen: der Aufbau, die Angleichung, die Härtung.
Der Aufbau folgte einer Reihenfolge, die aus den vorangegangenen Stationen zwingend hervorging. Zuerst der Domänenkern mit den Statusdimensionen und der zugehörigen Historie, dann die Aggregate samt der Sprachregel als Invariante, danach die Persistenz, die Berechtigungen und das Navigationsgerüst. Erst darauf folgten die Views – und zwar einzeln, entlang der View-Briefs: der Themen-Arbeitsplatz, der Sprachfassungs-Editor, die Veröffentlichungssicht, die Redaktionstafel, die Verlaufsansicht, die Publikationsorte und zuletzt der Import. Diese Reihenfolge ist kein Zufall; sie ist die Briefs von oben nach unten abgearbeitet – mit einer einzigen Abweichung: Die Veröffentlichungssicht, der dramaturgische Kern, wurde vor die Redaktionstafel gezogen. Wer sich fragt, wozu der Umweg über Prozesskatalog und View-Briefs gut war, findet hier die Antwort: Die Umsetzung musste nichts mehr erfinden.
Ein Detail dieser Phase amüsiert mich im Rückblick. Der Domänenkern entstand zunächst mit deutschen Bezeichnern – Statusverlauf, Statuswechsel, Sprachfassung –, ganz so, wie wir im Gespräch darüber gesprochen hatten. Ein eigener Umbau räumte das später wieder auf und übersetzte den gesamten Code ins Englische, gut siebzehnhundert geänderte Zeilen über siebenundfünfzig Dateien. Was im ersten Teil als StatusHistory zu sehen war, hieß zuvor anders. Die fachliche Sprache des Gesprächs ist eben nicht automatisch die Sprache des Codes, und es ist besser, das früh zu entscheiden als spät.
Das Modell in der Oberfläche
Bislang habe ich über die Oberfläche geredet, ohne sie zu zeigen. Das ist unbefriedigend, denn die interessante Frage lautet ja, wie sich ein solches Modell in einer Oberfläche wiederfindet – und ob dabei etwas verlorengeht. Beginnen wir mit jener Maske, die den Kern des Ganzen trägt.
Die zweispaltige Veröffentlichungssicht, die im ersten Teil abgebildet ist, entsteht aus wenigen Zeilen:
FlexLayout columns = new FlexLayout();
columns.setWidthFull();
columns.getStyle().set("gap", "var(--lumo-space-l)");
columns.setFlexWrap(FlexLayout.FlexWrap.WRAP);
columns.add(acquisitionColumn(), productionColumn());
body.add(columns);
//Aufschlussreicher als das Layout ist jedoch,
//was in einer solchen Spalte geschieht:
private Div productionColumn() {
Select<ProductionStatus> select = new Select<>();
select.setItems(ProductionStatus.values());
select.setValue(publication.productionStatus());
select.addValueChangeListener(e -> {
if (e.getValue() != null && e.getValue() != publication.productionStatus()) {
publication.changeProductionStatus(e.getValue(), actor());
repo.persist();
render();
}
});
...
}
Diese zwölf Zeilen sind das eigentliche Argument. Das Auswahlfeld wird unmittelbar mit den Werten der Aufzählung gefüllt – keine Übertragungsobjekte, keine Zeichenketten, keine zweite Pflege der Zustände zwischen Oberfläche und Modell. Der Ereignisbehandler ruft die gehärtete Domänenmethode direkt auf; zwischen dem Klick und dem Anhängen an die Statushistorie liegt kein Netzaufruf, keine Serialisierung, keine zweite Sprache. Der Typparameter sorgt überdies dafür, dass der Übersetzer meldet, wenn eine Dimension einen Wert erhielte, der ihr nicht gehört. Was im ersten Teil als Entflechtung begann, kommt hier an.

Abbildung 1: Die Veröffentlichungsübersicht der laufenden Anwendung — jede Zeile eine Veröffentlichung, Akquise- und Herstellungszustand nebeneinander.

Abbildung 2: Die Veröffentlichungssicht der laufenden Anwendung — links die Akquise, rechts die Herstellung, jede mit eigenem Zustand und Verlauf.

Abbildung 3: Die Verlaufsübersicht der laufenden Anwendung — Statuswechsel aller drei Dimensionen in einer Liste, jüngste zuerst, jede Zeile mit Absprung in ihre lückenlose Kette.
Die Redaktionstafel über vier Schritte
Wie sich eine solche Ansicht über die Zeit verbessert, lässt sich an der Redaktionstafel besonders schön ablesen. Sie wurde in vier Schritten zu dem, was sie heute ist, und jeder Schritt hatte einen benennbaren Auslöser. Die Verbesserungswünsche wurden dabei ebenso durchnummeriert wie zuvor die Prozesse, sodass jede Commit-Meldung ihren Anlass mitführt – ein kleiner Ordnungsaufwand, der sich beim Nachvollziehen auszahlt.
Schritt eins – die Anforderung aus dem View-Brief. Verlangt war eine Tafel, die den redaktionellen Fortschritt aller Teile auf einen Blick zeigt, mit einer Spalte je Arbeitszustand. Genau das entstand. Bewegt wurde eine Karte allerdings über ein Auswahlfeld, das auf jeder einzelnen Karte saß:
Select<EditorialState> move = new Select<>();
move.setItems(EditorialState.values());
move.setValue(part.editorialState());
move.setWidthFull();
move.addValueChangeListener(e -> { ... });
card.add(title, info, move);
Das erfüllte die Anforderung, war aber keine Tafel, sondern eine Liste mit Klappmenüs. Der View-Brief hatte das Ziehen und Ablegen zwar vorgesehen, doch die erste Umsetzung wählte den kürzeren Weg – ein Verhalten, das ich beim Assistenten häufiger beobachtet habe: Er erfüllt den Buchstaben der Anforderung, nicht ihren Geist, solange man nicht nachfasst.
Schritt zwei – der Wunsch nach echtem Ziehen und Ablegen. Nachgefasst wurde als Wunsch F4, und hier zeigt sich, was gemeint ist, wenn von durchgängigem Java die Rede ist:
DropTarget<Div> dropTarget = DropTarget.create(col);
dropTarget.addDropListener(e -> onDrop(state));
DragSource<Div> dragSource = DragSource.create(card);
dragSource.setDragData(part);
dragSource.addDragStartListener(e -> draggedPart = part);
dragSource.addDragEndListener(e -> draggedPart = null);
Kein JavaScript, keine Ereignisbehandlung im Browser, keine Vermittlung zwischen zwei Welten – Ziehen und Ablegen sind hier eine gewöhnliche Java-Schnittstelle. Und beide Bedienwege münden in dieselbe Domänenmethode, was verhindert, dass sich zwei Pfade auseinanderentwickeln:
private void onDrop(EditorialState target) {
if (draggedPart != null && draggedPart.editorialState() != target) {
draggedPart.changeState(target, actor());
repo.persist();
draggedPart = null;
refresh();
}
}
Bemerkenswert ist, dass das Auswahlfeld zunächst bewusst erhalten blieb; der Quelltext vermerkte es ausdrücklich als gleichwertigen, barrierefreien Rückfallweg für alle, die nicht mit der Maus ziehen können oder wollen.
Schritt drei – der Wunsch nach einer zweiten Ansichtsform. Im Alltag stellte sich heraus, dass die Tafel bei vielen Teilen unübersichtlich wird und eine tabellarische Darstellung derselben Daten fehlt. Mit dieser Ergänzung wurde das Auswahlfeld auf den Karten entbehrlich, denn die Tabelle bot ohnehin einen mausfreien Weg. Der dritte Schritt fügte also nicht nur etwas hinzu, sondern nahm etwas weg: Das Auswahlfeld verschwand samt seinem Import, und die Karte trägt seither nur noch Titel, Teil und Kennzeichen. Es gehört zur Ehrlichkeit dieser Erzählung, dass eine solche Rücknahme leichter fällt, wenn ein Assistent die Änderung ohne nennenswerten Aufwand durchzieht; von Hand hätte ich das funktionierende Auswahlfeld vermutlich stehen lassen.

Abbildung 1: Die Redaktionstafel der laufenden Anwendung — Spalten je Arbeitszustand, Karten ohne das entbehrlich gewordene Auswahlfeld.

Abbildung 2: Dieselben Daten als Tabellenansicht — nach Arbeitszustand gruppiert.
Schritt vier – der Einwand eines Prüfwerkzeugs. Der letzte Schritt kam nicht aus dem Gespräch. Die Tafel hielt die gefilterten Teile in einem Feld zwischengespeichert, um sie zwischen Spalten- und Tabellenansicht zu teilen – bequem, aber ein Zustand, der veralten kann und den die statische Analyse zu Recht bemängelte. An seine Stelle trat eine Methode, die bei Bedarf neu berechnet:
private List<Part> filteredParts() {
PublicationsFilter filter = PublicationsFilter.current();
return repo.issues().stream()
.filter(filter::matchesTitle)
.flatMap(i -> i.parts().stream())
.toList();
}
Vier Schritte: aus einer Liste mit Klappmenüs wurde eine Tafel, aus der Tafel eine schlankere Tafel mit zwei Ansichtsformen, und am Ende verschwand ein Zwischenspeicher, der Fehler hätte anziehen können. Später kamen weitere Wünsche hinzu – zusammengesetzte Suchfilter und eine wahlweise Und- oder Oder-Verknüpfung der Tags –, doch das Muster blieb dasselbe. Keiner dieser Schritte ist für sich genommen bemerkenswert. Bemerkenswert ist, dass sie in dieser Dichte stattfanden, denn dass eine Verbesserung kaum noch ins Gewicht fällt, entscheidet darüber, ob man sie überhaupt vornimmt.
Zuletzt eine Kleinigkeit, die mich freut, weil sie die Methode aus dem ersten Teil bis in den Quelltext trägt. Jede Ansicht nennt in ihrer Beschreibung die Prozesse, die sie bedient:
/**
* Served processes: P0008–P0013.
*/
@Route(value = PublicationView.NAV, layout = MainLayout.class)
@VisibleFor(AuthorizationRole.USER)
public class PublicationView extends Composite<VerticalLayout>
implements HasUrlParameter<String>, I18nSupport {
Wer in einem halben Jahr wissen will, warum es diese Maske gibt, findet die Antwort dort, wo er ohnehin nachsieht.
Die Naht: eigene Optik in fünfunddreißig Zeilen
Nun zu jener Naht, die ich am Ende des ersten Teils offen benannt hatte: Der Entwurf war ein freies Frontend-Mock, Vaadin bringt seine eigene visuelle Sprache mit. Wie schließt man das? Die Antwort fiel kleiner aus, als ich erwartet hatte. Ein einziger Commit brachte die IBM-Plex-Schriften und die Farbpalette in die Anwendung, und er umfasste fünfunddreißig geänderte Zeilen in einer einzigen Stildatei. Das ist möglich, weil sich Vaadins Lumo-Gestaltung über CSS-Variablen umdefinieren lässt – man tauscht Schrift, Farben und Rundungen aus, ohne eine Komponente anzurühren. Die Naht war also real, aber schmal. Wer eine eigene Optik will, bekommt sie; er muss nur wissen, dass sie in der Stildatei und nicht im Java-Code entsteht.
Die Brücke: der Import als ETL
Zwischen dem alten und dem neuen System steht eine Brücke, und sie ist von anderer Art als alles, was bisher entstanden ist. Sie gehört nicht zur Anwendung, sondern zum Übergang. Sie wird gebraucht, solange Daten in ClickUp liegen, und verschwindet, sobald sie es nicht mehr tun. Ein Wegwerfstück also — aber eines, von dem der gesamte Umzug abhängt, denn keine einzige Zuordnung zwischen Quelle und Ziel ergibt sich von selbst. Genau deshalb ist der Import als ETL gebaut und nicht als ein Skript, das einmal durchläuft und hofft.
Der Nutzen der Dreiteilung liegt in der strikten Trennung der drei Akte. Die Extraktion holt die Aufgabenliste aus dem Netz, schreibt sie unverändert auf die Platte und ist damit erledigt. Umwandlung und Laden arbeiten anschließend ausschließlich gegen diese örtliche Kopie und lassen sich beliebig oft wiederholen. Das ist keine Formalie, sondern der Kern der Sache: Die interessante Arbeit steckt in der Umwandlung, und Umwandlungen gerät man selten beim ersten Versuch richtig. Wer für jeden Versuch erneut über das Netz greift, macht aus einer schnellen Schleife eine zähe Angelegenheit und liefert sich obendrein den Mengenbegrenzungen der fremden Schnittstelle aus.
Die Rohkopie besteht aus zwei Dateien, dem JSON-Dokument und einem Vermerk mit dem Zeitpunkt der Extraktion. Bemerkenswert ist, was dabei aufbewahrt wird: sämtliche Felder der Quelle, nicht nur jene, die die Umwandlung derzeit verwendet. Diese Zurückhaltung ist Absicht. Was man heute nicht braucht, kann man später nicht mehr nachholen, wenn die Quelle abgeschaltet ist. Der Speicher selbst bleibt bewusst anspruchslos — er bekommt sein Verzeichnis übergeben und die Zeitangabe ebenfalls, statt eine Uhr zu befragen, worin er dem Verzicht auf Zeitstempel im Modell folgt. Auf der Netzseite genügt der HTTP-Client des JDK; ein Rahmenwerk wird an keiner Stelle gebraucht.
Der eigentliche Akt der Umwandlung ist jener, auf den der erste Teil dieses Berichts hinauslief: die Verteilung des konflierten Statusfeldes. Aus der Analyse ist eine Abbildungstabelle geworden, aus der Abbildungstabelle eine ausführbare Fallunterscheidung. Und hier ist eine Einschränkung offen zu benennen, die der Umzug nicht auflösen kann. Gefüllt wird allein die redaktionelle Dimension. Produktion und Akquise waren in ClickUp nie eigenständig verzeichnet, und keine Umwandlung wurde erfunden, was nie erhoben wurde. Die übernommenen Teile beginnen deshalb in den neutralen Anfangszuständen der beiden anderen Dimensionen. Die Entflechtung wirkt nach vorn, nicht rückwirkend — sie stellt das Modell bereit, in dem sich die Unterscheidung von nun an führen lässt.
Zwei Entscheidungen an dieser Stelle halte ich für die lehrreichsten des ganzen Moduls. Erstens fällt jeder unbekannte Statuswert nicht heraus, sondern in den Rückstand. Ein Datensatz, der nicht zugeordnet werden kann, geht nicht verloren, sondern landet sichtbar dort, wo ihn jemand wieder aufgreifen muss. Zweitens führt der Bericht des Laufes nicht bloß Zählwerte mit, sondern eine nach Häufigkeit ausgewertete Zuordnung der Form „Quellwert wurde zu Zielzustand”. Damit wird der Import zum Prüfmittel: Man liest ab, welcher der sechzehn ClickUp-Werte wie oft auf welchen redaktionellen Zustand fiel, und hält die am Schreibtisch entworfene Abbildungstabelle gegen den tatsächlichen Bestand.
Bleibt die Wiederholbarkeit, und sie beruht auf einem einzigen Feld. Jeder übernommene Vorgang vermerkt seine Herkunft, nämlich die Kennung der ClickUp-Aufgabe. Findet das Laden eine bereits vermerkte Herkunft, so übergeht es den Datensatz. Ein zweiter Lauf legt daher nichts an, sondern meldet lediglich, was er übersprungen hat. Erst diese Eigenschaft macht die Brücke im Alltag brauchbar: Man importiert, betrachtet die Verteilung, verbessert die Zuordnung, importiert erneut — und muss zwischendurch nichts aufräumen.
Die Selbstbeschreibung, die im Modul steht, trifft die Sache genau: in der Anwendung ein Wegwerfstück, in der Entwicklung ein Arbeitstier. Vollständig ehrlich wird die Bilanz allerdings erst später. Die Extraktion läuft die Seiten der fremden Schnittstelle in einer Schleife ab, und dass sie das anfangs nicht tat, war der folgenreichste Fehler des ganzen Vorhabens. Dass er überhaupt auffiel, verdankt sich der Rohkopie: Weil der Extrakt vollständig auf der Platte liegt, lässt sich zählen, was angekommen ist, und mit dem vergleichen, was vorhanden sein müsste.
Prüfen ohne Browser
Jede bisherige Station dieses Berichts führt auf dieselbe unbequeme Frage zu, und ich habe sie bislang nur gestreift. Wenn ein Assistent den Code schreibt und anschließend die Tests dazu, was beweisen diese Tests dann eigentlich? Sie prüfen, was der Assistent verstanden hat. Ein Missverständnis pflanzt sich vom Code in den Test fort und wird dort nicht entlarvt, sondern bestätigt. Wer so arbeitet und sich auf grüne Balken verlässt, hat sich eine Maschine gebaut, die ihm zustimmt.
Zunächst zur handwerklichen Seite, denn ohne sie gäbe es überhaupt nichts zu prüfen. Eine Vaadin-Oberfläche lässt sich ohne Browser testen: Die Ansicht wird in derselben Laufzeit erzeugt, in der auch die Prüfung läuft, und man greift auf den echten Komponentenbaum zu, statt auf gerendertes HTML. Kein Browser, kein Fernsteuerungsprotokoll, keine wartenden Sekunden – und vor allem keine Sprödigkeit gegenüber Beschriftungen und Positionen. Eine solche Prüfung sieht so aus:
@Test
@DisplayName("renders both lifecycle columns with their initial states")
void rendersTwoColumns() {
UI.getCurrent().navigate(PublicationView.class, vId.toString());
assertEquals("Publication", $view(H1.class).first().getText());
List<String> spanTexts = $view(Span.class).all().stream()
.map(Span::getText).collect(Collectors.toList());
assertTrue(spanTexts.contains("ACQUISITION"), "left column is Acquisition");
assertTrue(spanTexts.contains("PRODUCTION"), "right column is Production");
assertTrue(spanTexts.contains("REQUESTED"), "acquisition starts at REQUESTED");
assertTrue(spanTexts.contains("PLANNED"), "production starts at PLANNED");
assertEquals(2, $view(Select.class).all().size(),
"one advance-Select per dimension");
}
Bemerkenswert daran ist weniger die Technik als vielmehr das, was hier eigentlich geprüft wird. Die Behauptung, die dieser ganze Beitrag trägt – zwei unabhängige Lebenszyklen an einem Objekt –, ist an dieser Stelle zu einer Zusicherung geworden, die bei jedem Übersetzungslauf nachgerechnet wird. Entfernte jemand eine der beiden Spalten, fiele der Test. Die Entflechtung aus der Analyse ist damit nicht nur modelliert und gezeichnet, sondern festgenagelt.
Ebenso aufschlussreich ist die Vorbereitung solcher Prüfungen. Sie bauen ihren Bestand vollständig selbst auf – einen Publikationsort, ein Thema, einen Teil, eine Sprachfassung, eine geplante Veröffentlichung – und legen ihn in einer zweiten, rein speicherbasierten Persistenzimplementierung ab, die neben der dateibasierten Persistenz implementiert ist. Dass ein Objektgraph sich derart leicht durch eine Attrappe ersetzen lässt, ist ein stiller Vorzug des Verzichts auf eine Datenbank: Es gibt kein Schema, das man vorher anlegen müsste, und keinen Server, den man hochfahren müsste.
Inzwischen umfasst das Vorhaben einundsechzig Testklassen mit knapp dreihundert Prüfungen; einundzwanzig dieser Klassen sind browserlose Ansichtsprüfungen. Und genau hier kehrt die eingangs gestellte Frage zurück: Diese Zahlen beeindrucken, beweisen aber für sich genommen nichts. Der abgeschnittene Import aus der vorigen Station war von Tests umgeben, die sämtlich grün liefen.
Deshalb braucht es Prüfmittel, die von außen kommen – die also nicht wissen, was der Assistent gemeint hat. Das Erste ist die statische Analyse, die im Code der Publikationsverwaltung siebzehn Beanstandungen fand: fehlende Serialisierungskennungen, nach außen durchgereichte veränderbare Sammlungen. Nichts davon war ein Denkfehler, alles davon war handwerkliche Nachlässigkeit, und keiner der Tests hätte es je bemerkt.
Das zweite und wirksamere Mittel ist die Mutationsprüfung, die genau die Frage beantwortet, um die es hier geht. Sie verändert den Code an vielen Stellen absichtlich geringfügig – kehrt eine Bedingung um, vertauscht einen Rückgabewert, entfernt einen Aufruf – und lässt anschließend die Tests laufen. Bemerkt keiner der Tests die Veränderung, so hat man einen Test, der zwar durchläuft, aber nichts absichert. Wo die übliche Abdeckungsmessung nur zählt, welche Zeilen durchlaufen wurden, fragt die Mutationsprüfung, ob deren Verhalten überhaupt festgelegt ist. Für Tests, die von derselben Instanz stammen wie der Code, ist das die einzig ehrliche Gegenprobe.
Die Ausgestaltung im Projekt halte ich für die eigentliche Lehre dieses Kapitels. Ein einzelner globaler Schwellenwert wäre unbrauchbar, weil er entweder die Oberfläche überfordert oder den Kern unterfordert. Stattdessen führt eine eigene Datei paketweise Untergrenzen, und deren Kommentar benennt die Erwartungen offen: Die tragenden Sicherheits- und Domänenpakete liegen zwischen fünfundsiebzig und fünfundneunzig Prozent, die Ansichtspakete liegen bewusst weit darunter. Die Grenzen sind bewusst zwei bis drei Punkte unter dem gemessenen Stand angesetzt, damit gewöhnliche Umbauten sie nicht auslösen, ein echter Rückschritt aber schon; angehoben werden dürfen sie erst nach dauerhaft besserem Stand, gesenkt nur mit schriftlicher Begründung.
Eine solche Begründung steht tatsächlich in der Datei, und sie ist lehrreicher als jede Erfolgsmeldung: Die Gesamtuntergrenze wurde gesenkt, als neue Oberflächenbestandteile hinzukamen, die viele Mutanten in reinem Zusammenbaucode erzeugen – Code also, der Komponenten zusammensteckt und in dem es wenig zu behaupten gibt. Das ist der ehrliche Umgang mit einer Kennzahl: nicht sie schönreden, sondern aufschreiben, warum sie sich bewegt hat.

Abbildung 1: Die Prüfebenen und was jede von ihnen prüft – innere Stimmigkeit lässt sich automatisieren, Richtigkeit nicht.
Bleibt der Rest, den kein Werkzeug abdeckt. Weder die statische Analyse noch die Mutationsprüfung hätten bemerkt, dass der Import bei einhundert Aufgaben abbricht, denn beide prüfen die Übereinstimmung von Code und Test, nicht die von Ergebnis und Wirklichkeit. Das bleibt am Menschen hängen, der seinen Datenbestand kennt. Die Arbeitsteilung, die sich daraus ergibt, ist unspektakulär, aber tragfähig: Der Assistent stellt her, die Werkzeuge prüfen die innere Stimmigkeit, und der Mensch prüft, ob das Ganze überhaupt das Richtige ist.
Fehlerbehebung und Alltagstauglichkeit
Die Fehlerbehebung brachte den lehrreichsten Fund des gesamten Vorhabens. Der Import lief zunächst tadellos – zu tadellos. Er holte einhundert Aufgaben und meldete Erfolg. Dass die ClickUp-Schnittstelle ihre Ergebnisse seitenweise ausliefert und stillschweigend nach hundert Einträgen abschneidet, fiel erst auf, als die Zahlen nicht zum Bestand passten. Die Behebung lief die Seiten in einer Schleife ab und zog sie zu einem einzigen Rohdokument zusammen, mit einer harten Obergrenze, damit eine sich falsch verhaltende Schnittstelle nicht in eine Endlosschleife führt. Das ist die Sorte Fehler, die kein Werkzeug für einen findet: Es gab keine Ausnahme, keinen roten Test, keine Warnung – nur ein Ergebnis, das plausibel aussah und falsch war. Hier zeigt sich die Grenze der Beschleunigung recht genau. Das Werkzeug schrieb den Import mühelos; dass er zu wenig importierte, bemerkte der Mensch, der seinen Datenbestand kennt.
Weniger dramatisch, aber bezeichnend war die zweite Gruppe von Befunden. Eine statische Prüfung fand siebzehn Beanstandungen im Code der Publikationsverwaltung, sämtlich von der langweiligen Sorte: fehlende Serialisierungskennungen an den Views, nach außen durchgereichte veränderbare Sammlungen, die besser defensiv kopiert werden. Behoben wurden sie durch sechsundfünfzig hinzugefügte Zeilen. Es ist genau jene Schicht handwerklicher Sorgfalt, die ein Assistent zuverlässig übersieht, solange niemand ihn darauf stößt – und die ein Prüfwerkzeug ebenso zuverlässig findet. Dass beide zusammen besser sind als jeder für sich, ist die eigentliche Lehre.
In dieser Phase hat sich auch bestätigt, was die View-Briefs vorhergesagt hatten. Die Redaktionstafel und die Verlaufsansicht blieben Eigenbauten; die Tafel ist mit dreihundertvierundfünfzig Zeilen die zweitgrößte der sieben Publikationsansichten, samt selbst verdrahtetem Ziehen und Ablegen zwischen den Spalten. Das war kein böses Erwachen, weil es angekündigt war – und darin liegt der ganze Wert einer Entwurfsplanung, die ihre Lücken benennt.
Was danach kam, ist die eigentliche Arbeit an einer Anwendung, die man täglich benutzen will, und sie füllt die Freigaben zweiunddreißig bis vierzig: Filter über mehrere Kriterien, Tag-Verknüpfungen wahlweise als Und oder Oder, verstellbare Spaltenbreiten, Rücksprungwege, die dorthin führen, wo man hergekommen ist, editierbare Einträge an Ort und Stelle. Diese Phase lässt sich nicht abkürzen, denn sie besteht aus lauter kleinen Beobachtungen, die man erst macht, wenn man das eigene Werkzeug wirklich benutzt. Der Assistent setzt sie schnell um; der Anwender muss sie bemerken.
Unterm Strich hat sich die Rechnung gehalten, aber nicht so, wie es die naive Erwartung nahelegt. Der Weg vom leeren Verzeichnis zur benutzbaren Anwendung war für einen Einzelnen neben der laufenden Arbeit bemerkenswert kurz. Die Beschleunigung liegt jedoch fast vollständig in der Herstellung – beim Schreiben von Klassen, Views, Tests und Übersetzungen. Sie liegt nicht im Entdecken. Der abgeschnittene Import, der falsche Rücksprung, die unbequeme Bedienung: All das musste jemand bemerken, der weiß, wie die Sache sich anfühlen soll.
Die ehrliche Bilanz
Ein Beitrag, der von einer gekippten Rechnung erzählt, schuldet dem Leser beide Spalten dieser Rechnung. Mein erster Entwurf dieses Kapitels hatte einen Fehler, den ich erst beim Nachlesen bemerkte: Er führte die Kosten der Eigenlösung penibel auf und stellte ihnen einen Mietdienst gegenüber, dessen Kosten unbeziffert blieben. Das ist keine Bilanz, sondern eine einseitige Aufstellung. Also beides.
Der Posten, der sich nicht wegdiskutieren lässt, ist die Wartung. Jede Bibliothek altert, Sicherheitslücken werden gemeldet, eine neue Java-Version erscheint – und niemand außer mir wird sich darum kümmern. Diese Arbeit endet nicht. Sie ist allerdings kleiner, als das Wort vermuten lässt, und Genauigkeit hilft hier mehr als Dramatik: Es handelt sich um eine Anwendung mit einem einzigen Benutzer, ohne Verfügbarkeitszusage, ohne Mandantentrennung und mit wenigen Abhängigkeiten. Der Aufwand bleibt entsprechend überschaubar.
Entscheidender als die Größe dieses Postens ist seine Natur, und hier liegt der eigentliche Gewinn. Meine Wartungslast gehorcht mir. Ich entscheide, wann ich eine Abhängigkeit hebe; rühre ich ein Jahr lang nichts an, läuft die Anwendung weiter wie zuvor. Die Veränderungen eines Mietdienstes hingegen widerfahren mir. Ich kann sie weder terminieren noch ablehnen: Preise steigen, Funktionen verschwinden oder werden umgebaut, eine Oberfläche wird neu geordnet, ein Anbieter wird übernommen. Beides sind Kosten, doch nur eine davon steht unter meiner Kontrolle. Diese Unterscheidung zwischen beherrschten und widerfahrenden Kosten ist der sachliche Kern dessen, was ich mit Souveränität meine.
Damit zur zweiten Spalte, die im Entwurf fehlte. ClickUp kostete nicht nur eine monatliche Gebühr. Es kostete die täglichen Umwege aus dem ersten Kapitel – die Notbehelfe über Tags, das überladene Statusfeld, die Unmöglichkeit, eine einzelne Frage sauber zu stellen. Es kostete die Bedienung einer Funktionsfülle, von der ich nur einen schmalen Ausschnitt nutzte. Und es kostete etwas, das erst im Ernstfall sichtbar wird: die Fähigkeit zum Ausstieg. Wer weder seine Daten noch den Quelltext besitzt, kann bei einer unliebsamen Änderung nicht wechseln, sondern nur hinnehmen. Diese Posten erscheinen auf keiner Rechnung, und genau deshalb übersieht man sie jahrelang.
Der dritte Posten ist die Abhängigkeit, und hier muss ich meine eigenen Grundsätze prüfen. Ich halte mich gern nah am JDK; weder Spring noch Jakarta EE kommen in dieser Anwendung zum Einsatz. Das ist keine Marotte, sondern folgt aus dem Zuschnitt: Ein Anwendungsserver und ein Rahmenwerk zur Abhängigkeitsverwaltung lösen Aufgaben, die ich hier nicht habe – Mandanten, verteilte Transaktionen, austauschbare Implementierungen in großen Mannschaften. Vaadin Flow läuft auf einem eingebetteten Jetty, die Verdrahtung besorgt gewöhnlicher Java-Code, und was übrig bleibt, lässt sich vollständig lesen und überblicken. Vaadin allerdings ist alles andere als eine kleine Abhängigkeit – ein umfangreiches Rahmenwerk mit eigener Laufzeitwelt und eigenem Aufbauwerkzeug. Wer es einsetzt, tauscht die Bruchstelle zwischen Java und JavaScript gegen die Bindung an einen Anbieter. Ich halte den Tausch für vorteilhaft, weil er genau jene Reibung beseitigt, die mich sonst am meisten kostete, und weil das Ergebnis quelloffen bei mir liegt und übersetzbar bleibt, auch wenn ich nichts weiter aktualisiere. Ein Tausch bleibt es dennoch. Hinzu kommen die Objektablage und ein Werkzeug zum Lesen von JSON; von „nur das JDK” ist die Anwendung weit entfernt. Das Ziel war stets Zurückhaltung, nicht Askese.
Der vierte Posten betrifft die Kehrseite der Datenhoheit. Meine Daten liegen jetzt bei mir – und damit auch die Verantwortung dafür. Über Sicherungen, defekte Datenträger und einen Umzug auf neue Hardware musste ich bei ClickUp nie nachdenken. Nun schon. Immerhin ist dieser Posten lösbar und einmalig einzurichten, und er hat eine Kehrseite, die für mich schwerer wiegt: Eine Sicherung, die ich selbst anfertige, kann ich auch selbst zurückspielen – und ich weiß, in welchem Format sie vorliegt.
Über den Assistenten wurde ebenfalls etwas gesagt, was der bisherige Bericht nur streift. Er hat die Herstellung außerordentlich beschleunigt, besitzt aber kein Urteil darüber, ob das Hergestellte richtig ist. Der abgeschnittene Import aus der vorigen Station ist das beste Beispiel: sauberer Code, grüne Tests, falsches Ergebnis. Auch die Tests tragen diese Schwäche in sich, denn sie prüfen, was der Assistent verstanden hat – ein Missverständnis pflanzt sich vom Code in den Test fort und wird dort noch bestätigt. Wer so arbeitet, braucht Prüfmittel von außen: statische Analyse, eine Bewertung der Testgüte durch gezielte Verfälschung des Codes und vor allem den eigenen Blick. Diese Einsicht spricht nicht gegen den Weg, aber sie bestimmt, wie man ihn geht.
Zwei Punkte lasse ich ohne Trostpflaster stehen, weil ihnen keines zusteht. Der erste ist die Abhängigkeit von einer einzigen Person: Fiele ich aus, gäbe es niemanden, der diese Anwendung weiterpflegt. Bei einem Mietdienst wäre das anders, und dagegen hilft kein Argument. Der zweite ist der Zeitpunkt dieses Beitrags: Die Anwendung ist noch jung. Ob sie sich über Monate im Alltag bewährt, ob mir in einem halben Jahr Entscheidungen auf die Füße fallen, die heute vernünftig aussehen – das weiß ich schlicht nicht. Wer hier Gewissheit behauptet, hat sie nicht.
Offen geblieben ist überdies einiges. Die PDF-Erzeugung, einer der ursprünglichen Beweggründe, ist nicht umgesetzt und würde eine weitere bewusste Ausnahme vom Abhängigkeitsgrundsatz erfordern. Die kaufmännische Welt blieb außen vor – richtigerweise, aber sie fehlt. Der Import erfüllt seinen Zweck, doch der endgültige Umstieg mit Stichtag und Abgleich steht noch bevor.
Bleibt die Frage, wie die Rechnung nun aussieht. Ihre beiden Seiten verlaufen gegenläufig: Der Bauaufwand war einmalig und liegt hinter mir, die Miete wäre wiederkehrend und wüchse erfahrungsgemäß. Der Nutzen wirkt fort, solange ich schreibe, und er besteht nicht allein in gesparten Gebühren, sondern in etwas, das vorher unmöglich war – eine Anwendung, die meinen Ablauf abbildet, statt ihn in ein fremdes Modell zu pressen, Daten in einem Format, das ich kenne, und einen Quelltext, der ohne den Assistenten weiterlebt, der ihn schreiben half. Die Rechnung ist also gekippt, aber nicht trivial. Sie lautet nicht „Eigenbau ist jetzt umsonst”, sondern: Der Bauaufwand ist so weit gesunken, dass die dauerhafte Pflege zum entscheidenden Posten wird. Und diese Pflege ist, anders als die Veränderungen eines fremden Dienstes, eine Last, die man selbst in der Hand hält.
Ausblick
Drei Schritte sind absehbar, und sie stehen in einer natürlichen Reihenfolge.
Der nächste ist der endgültige Umstieg. Bislang läuft die neue Anwendung neben ClickUp her, gespeist durch einen Import, der sich beliebig wiederholen lässt. Irgendwann braucht es einen Stichtag, einen letzten Abgleich und die Entscheidung, den alten Dienst nicht mehr zu füttern. Dieser Moment ist unspektakulär und zugleich der eigentliche Prüfstein: Erst wenn ich das Werkzeug ohne Rückfalloption benutze, zeigt sich, ob es taugt.
Der zweite Schritt ist die PDF-Erzeugung – jener Beweggrund aus dem ersten Kapitel, der bislang unerfüllt blieb. Aus dem Bestand heraus Sammelausgaben zu setzen, war mit einem Aufgabenverwalter nie zu denken, und nun ist es lediglich eine Frage der Arbeit. Sie wird eine weitere bewusste Ausnahme von meinem Abhängigkeitsgrundsatz verlangen, denn für das Setzen von Dokumenten bringt das JDK nichts mit. Dass ich diese Ausnahme mache und nicht verschweige, ist mir lieber, als so zu tun, als ließe sich alles aus Bordmitteln bauen.
Der dritte Schritt ist die kaufmännische Welt, die ich im ersten Teil bewusst ausgeklammert habe: Auftraggeber, Vergütungsmodelle, Rechnungen, eine Vorschau auf die zu erwartenden Einnahmen. Das schlanke Modell wurde von Anfang an als echter Teilausschnitt entworfen, nicht als Provisorium, und der Übergang erfolgt daher durch Hinzufügen statt durch Umschreiben. Ob diese Zusicherung trägt, wird sich zeigen – es ist die Art von Versprechen, das man sich selbst gibt und das die Wirklichkeit gelegentlich zurückweist.
Und darüber hinaus? Der Reiz einer eigenen Anwendung liegt darin, dass jede kleine Beobachtung im Arbeitsalltag unmittelbar zu einer Änderung führen kann, statt nur zu einem Eintrag in einer Wunschliste, die jemand anderes verwaltet. Genau darin besteht der Unterschied, den ich am Anfang suchte, ohne ihn benennen zu können: Ich habe nicht ein besseres ClickUp gesucht, sondern ein Werkzeug, das mir gehorcht statt mich zu formen. Ob die Rechnung auf Dauer aufgeht, weiß ich in einem Jahr. Dass sie sich überhaupt aufmachen ließ, ist neu – und das ist die eigentliche Nachricht dieses Beitrags.