Entwickler starten Builds und gehen Kaffee holen. Sie checken Slack. Sie wechseln in eine andere Aufgabe. Wenn der Build fertig ist, haben sie den Faden verloren, an dem sie gearbeitet haben. Dreissig Minuten Build-Zeit kosten nicht dreissig Minuten. Sie kosten die Konzentration jedes Entwicklers im Team, mehrmals täglich.
Ich verbrachte zwei Jahre eingebettet in ein Team, das genau damit zu kämpfen hatte. Die Siemens SIMATIC AX-Plattform, ein Automatisierungs-Engineering-Tool der nächsten Generation, hatte eine einzige massive Codebase, bei der die Build-Zeiten auf über 30 Minuten angestiegen waren. Das Team hatte alles versucht, um die Builds zu beschleunigen. Mehr Caching. Mehr Rechenleistung. Andere Bundler. Nichts hielt an.
Die tatsächlichen Kosten langsamer Builds
Build-Zeit ist leicht zu messen, aber leicht zu unterschätzen. Ein 30-Minuten-Build fügt einem Entwickler nicht einfach 30 Minuten zu seinem Tag hinzu. Er fragmentiert seine Aufmerksamkeit und schafft tote Zeit, die sich über das Team multipliziert.
Stellen Sie sich ein Team von 20 Entwicklern vor, die jeweils 4 bis 5 Builds pro Tag auslösen. Bei 30 Minuten pro Build sind das täglich 40 bis 50 Stunden Wartezeit. Aber die eigentlichen Kosten liegen im Context-Switching. Forschungen zur Entwicklerproduktivität zeigen konsistent, dass es 15 bis 20 Minuten dauert, um nach einer Unterbrechung wieder tiefe Konzentration zu erreichen. Ein 30-Minuten-Build schafft jedes Mal eine 45 bis 50 Minuten lange Lücke in produktiver Arbeit.
Langsame Builds verändern auch das Verhalten. Entwickler bündeln Änderungen, anstatt häufig zu iterieren. Sie testen seltener. Sie pushen grössere Commits. Grössere Commits bedeuten schwierigere Code-Reviews, mehr Merge-Konflikte und mehr Bugs, die durchschlüpfen. Die Build-Zeit verlangsamt nicht nur die Auslieferung. Sie verschlechtert den gesamten Entwicklungs-Workflow.
Warum der Build langsam ist
Die reflexartige Reaktion auf langsame Builds ist die Optimierung des Builds selbst. Besseres Caching. Parallele Kompilierung. Schnellere Hardware. Inkrementelle Builds. Diese Ansätze funktionieren bis zu einem gewissen Punkt. Aber wenn Ihr Build langsam ist, weil Ihre Codebase ein Monolith ist, behandelt die Optimierung des Builds das Symptom.
In einer monolithischen Codebase hängt alles von allem ab. Ändern Sie eine Datei und das Build-System muss den gesamten Abhängigkeitsgraph auswerten, um herauszufinden, was betroffen sein könnte. Selbst bei inkrementellen Builds bedeutet die schiere Anzahl von Verbindungen zwischen Modulen, dass die meisten Änderungen genug vom Graph berühren, um einen nahezu vollständigen Rebuild auszulösen.
Der Build ist nicht langsam, weil das Build-Tool schlecht ist. Der Build ist langsam, weil die Codebase es für jedes Build-Tool unmöglich macht, schnell zu sein. Der Code war nie für unabhängige Kompilierung strukturiert. Er wuchs organisch, mit verschiedenen Funktionsbereichen, die auf Weisen verwoben sind, die saubere, schnelle inkrementelle Builds unmöglich machen.
Modularisierung als Zuständigkeitsfrage
Das ist die Erkenntnis, die uns von 30 Minuten auf unter fünf brachte: Modularisierung ist kein Build-Tool-Problem. Es ist eine Frage der Zuständigkeiten.
Wenn ein Team eine einzige grosse Codebase betreut, gibt es keinen natürlichen Anreiz, Dinge getrennt zu halten. Alles landet im selben Repository. Abhängigkeiten schleichen sich über Grenzen ein, weil es keine Grenzen gibt. Gemeinsame Utilities häufen sich an, weil es einfacher ist, eine Funktion zur bestehenden Utils-Datei hinzuzufügen, als darüber nachzudenken, wohin sie wirklich gehört.
Modularisierung zwingt Sie, organisatorische Fragen zu beantworten. Wer ist für diesen Code verantwortlich? Wer entscheidet, was hineinkommt und was draussen bleibt? Wer ist zuständig, wenn es bricht? Das sind keine technischen Fragen. Das sind Teamstruktur-Fragen. Und wenn Sie sie nicht beantworten, wird die technische Trennung nicht halten. Jemand wird eine modulübergreifende Abhängigkeit hinzufügen, weil es bequem ist, und sechs Monate später sind Sie zurück bei einem Monolithen mit zusätzlichen Schritten.
Natürliche Grenzen identifizieren
Das grösste Risiko bei der Modularisierung ist das Schneiden an den falschen Stellen. Zu aggressiv aufgeteilt entsteht ein verteilter Monolith: Module, die nichts Nützliches tun können, ohne fünf andere Module aufzurufen. Zu konservativ aufgeteilt hat man das Problem nicht wirklich gelöst.
Natürliche Grenzen folgen tendenziell einigen Mustern:
Feature-Bereiche, die sich unabhängig verändern. Wenn sich ein Teil der Anwendung häufig ändert, während ein anderer stabil ist, ist das eine Grenze. Der sich häufig ändernde Teil profitiert von schnellen Builds und unabhängigem Deployment. Der stabile Teil profitiert davon, nicht gestört zu werden.
Verschiedene benutzerseitige Domänen. Ein Editor-Modul, ein Konfigurations-Modul, ein Monitoring-Dashboard. Diese bedienen unterschiedliche Nutzerbedürfnisse und umfassen typischerweise verschiedene Teams. Sie sind natürliche Kandidaten für die Trennung.
Gemeinsame Infrastruktur versus Produktfeatures. Authentifizierung, Logging, Design-System-Komponenten und API-Clients sind Infrastruktur. Sie ändern sich selten und werden überall verwendet. Produktfeatures sind das Gegenteil: Sie ändern sich häufig und interagieren mit bestimmten Teilen des Systems. Die Trennung dieser beiden Schichten verhindert, dass Produktfluktuation die gemeinsame Infrastruktur destabilisiert.
Eine nützliche Faustregel: Wenn ein Modul einem nicht-technischen Stakeholder in einem Satz beschrieben werden kann, hat es wahrscheinlich die richtige Grösse. "Das Modul, das die Wechselrichter-Konfiguration handhabt" ergibt Sinn. "Das Modul, das Wechselrichter-Konfiguration, Benutzerauthentifizierung und das Benachrichtigungssystem handhabt" bedeutet, dass man wahrscheinlich nicht genug aufgeteilt hat.
Der Migrationspfad
Man kann nicht sechs Monate die Feature-Auslieferung stoppen, um die Codebase umzustrukturieren. Die Migration muss inkrementell stattfinden, parallel zur normalen Produktentwicklung. Das ist der Ansatz, der bei SIMATIC AX funktionierte:
Den Abhängigkeitsgraph kartieren. Bevor man auch nur eine Zeile Code anfasst, muss man verstehen, was von was abhängt. Die Imports, den gemeinsamen State, die Laufzeit-Kopplung visualisieren. Die meisten Teams sind überrascht, was sie finden. Abhängigkeiten, die sie für sauber hielten, erweisen sich als zirkulär. Module, die sie für unabhängig hielten, teilen versteckten State.
Das einfachste Modul zuerst extrahieren. Den Teil der Codebase mit den wenigsten eingehenden Abhängigkeiten finden und in ein eigenes Repository mit einer eigenen Build-Pipeline extrahieren. Das gibt dem Team Übung mit dem neuen Workflow und beweist, dass der Ansatz funktioniert, bevor man schwierigere Extraktionen angeht.
Verträge vor dem Aufteilen definieren. Bevor man ein Modul extrahiert, definiert man, wie es mit dem Rest des Systems kommuniziert. Welche Events sendet es aus? Welche APIs stellt es bereit? Welche Daten konsumiert es? Diese Verträge werden zur Schnittstelle, von der anderer Code abhängt, anstatt von den Interna des Moduls.
Ein Modul nach dem anderen verschieben. Jede Extraktion sollte eine begrenzte, umkehrbare Operation sein. Wenn etwas schiefgeht, kann man sie rückgängig machen. Während man Module herausbewegt, schrumpft der Monolith und der verbleibende Code wird leichter zu durchdenken. Jeder Schritt macht den nächsten Schritt einfacher.
Nicht nach Perfektion streben. Die erste Extraktion wird nicht sauber sein. Es wird Kompromisse geben: eine gemeinsame Abhängigkeit, die beide Seiten benötigen, ein State-Objekt, das nicht sauber in eines der Module passt. Die Unordnung akzeptieren und sie in nachfolgenden Iterationen bereinigen. Auf einen perfekten Plan zu warten ist der Grund, warum Modularisierungsbemühungen nie anfangen.
Build-Pipeline-Architektur
Sobald Code in unabhängige Module getrennt ist, muss die Build-Pipeline diese Unabhängigkeit widerspiegeln. Jedes Modul sollte haben:
- Sein eigenes Repository mit seiner eigenen CI-Pipeline, die bei jedem Commit ausgeführt wird. Builds, die nur ein Modul berühren, sollten nur dieses Modul bauen.
- Seine eigene Test-Suite, die das Modul isoliert validiert. Contract-Tests verifizieren, dass das Modul seine Integrationspunkte einhält. Unit- und Integrationstests decken alles andere ab.
- Sein eigenes Deployment-Artefakt. Ein Modul, das seine Tests besteht, kann unabhängig deployed werden, ohne auf andere Module warten zu müssen.
Die Integrationsschicht (die Shell-Anwendung, die alle Module zusammensetzt) hat ihre eigene Pipeline, die validiert, dass die Module zusammenarbeiten. Aber diese Pipeline sollte schnell sein, weil sie nur Integration testet, nicht alles neu baut.
Bei SIMATIC AX reduzierte diese Umstrukturierung den Build von über 30 Minuten auf unter fünf. Aber die Build-Zeit war nur das messbare Ergebnis. Die tiefere Veränderung war, dass Teams unabhängig arbeiten konnten. Eine Änderung am Editor erforderte nicht, dass die gesamte Plattform neu gebaut und neu deployed wurde. Teams lieferten nach ihrem eigenen Zeitplan. Die Architektur skalierte mit der Organisation, anstatt gegen sie.
Worauf Sie achten sollten
Der verteilte Monolith. Wenn jedes Modul jedes andere Modul benötigt, um zu funktionieren, hat man nicht modularisiert. Man hat den Monolithen nur über mehrere Repositories verteilt. Das ist schlimmer als das Original, weil man nun die Komplexität der Verteilung hat, ohne die Vorteile der Unabhängigkeit. Saubere Verträge und minimale Kopplung sind das Gegenmittel.
Vorzeitige Abstraktion. Kein ausgeklügeltes Modul-Framework bauen, bevor man das erste Modul extrahiert hat. Einfach anfangen. Ein separates Repository mit einem klar definierten Build und einem klaren Vertrag ist genug. Orchestrierung, Versionierung und gemeinsames Tooling hinzufügen, wenn man gelernt hat, was man tatsächlich braucht.
Den kulturellen Wandel unterschätzen. Technische Modularisierung ist der einfache Teil. Der schwierige Teil ist die Veränderung, wie Teams über Zuständigkeiten und Grenzen denken. In einem Monolithen kann jeder alles ändern. In einem modularen System durchlaufen Änderungen am Code eines anderen Teams dessen Review-Prozess. Das fühlt sich anfangs langsamer an. Es ist schneller im Massstab, weil es den Koordinationsaufwand und die unerwarteten Fehler eliminiert, die mit ungeklärten Zuständigkeiten einhergehen.
Langsame Builds sind ein Symptom für ungeklärte Zuständigkeiten und verschlungene Abhängigkeiten. Schnellere Hardware und bessere Build-Tools können das Problem vorübergehend maskieren, aber nicht lösen. Modularisierung, echte Modularisierung, bei der Code-Grenzen mit Team-Grenzen übereinstimmen und jedes Modul unabhängig gebaut, getestet und deployed werden kann, ist die strukturelle Lösung. Es braucht Zeit. Es braucht Engagement im Team. Und es lohnt sich.
Verwandte Artikel: Design Systems als Infrastruktur · Wo AI in der Enterprise-Architektur wirklich hilft