Loading...

Was Accidental Complexity in der Entwicklung kostet

Architecture
22. März 2024
7 Minuten Lesezeit
Beitrag teilen:
Gefällt dir der Beitrag?
Du wirst den Newsletter lieben!

Anfangs setzen IT-Teams schnell neue Features um, dann wird die Entwicklungszeit meist länger. Accidental Complexity ist häufig die Ursache – wir erklären, wie sie entsteht und was sich dagegen tun lässt.

Es ist kein Geheimnis, dass Systeme über die Zeit komplexer und damit die Entwicklungszeiten länger werden. Das ist nicht zu vermeiden, wenn das Problem inhärent komplex ist, man spricht dann von Essential Complexity. In anderen Fällen wird die Umsetzung eines Features komplexer, als es sein müsste. Zwei Monate für ein eigentlich einfaches Feature? Ein klarer Fall von Accidental Complexity. Sie ist ärgerlich, kostet viel – und ist vermeidbar, wie wir an einigen praktischen Beispielen zeigen.

Es ist ein Gespräch, das viele ITler kennen, wenn sie an Projekten arbeiten, die schon lange in der Entwicklung sind: “Das dauert ungefähr vier Wochen”, sagt der leitende Entwickler. “Vier Wochen?”, erwiderst du ungläubig: “Und dann können wir die ersten auf den Kunden zugeschnittene Angebote per E-Mail verschicken?”

“Nein, in vier Wochen sind wir in der Lage, die Verhaltensdaten aus dem Bestandssystem mit dem CRM zu verknüpfen. Danach müssen wir noch das User-Konfigurationssystem um die E-Mail-Einstellungen erweitern und den Autorisierungsprozess an unseren Mailserver anknüpfen”, erklärt der leitende Entwickler leicht genervt. “Und dann ist noch eine Anpassung in unserem CI-System notwendig, um die Änderungen am Conversion-Service mit dem Bestandssystem zu deployen. Diese Änderungen brauchen zusätzlich ein bis zwei Sprints.”

Zwei Monate Entwicklungszeit für zwei Entwickler für dieses Feature? Du überschlägst im Kopf: Mit geschätzten Kosten von 700 Euro pro Entwicklertag und grob 80 Tagen Entwicklungszeit kostet dich die Entwicklung mehr als 50.000 Euro. Dabei sind die Kosten von anderen Beteiligten im Unternehmen noch gar nicht berücksichtigt. Lohnt sich das noch?

Essential vs. Accidental Complexity

Das kommt darauf an. Manchmal sind Projekte einfach komplex. Dann können wir unsere Schätzungen drehen und wenden, wie wir wollen – einfacher wird es nicht.

In den allermeisten Fällen können wir die hohen Schätzungen aber auf andere Faktoren zurückführen. Um ein Feature umzusetzen, müssen wir mit einer Komplexität umgehen, die nicht aus dem Feature selbst, sondern aus den Gegebenheiten um uns herum kommt.

Unser System ist aufgeteilt auf mehrere Services. Sie sind verteilt in einem Netzwerk. Wir haben Geo-Redundanzen. Es werden verschiedene Datenbanktechnologien verwendet. Das Deployment-System ist veraltet und wir benötigen ein Feature aus der neuen Version. Es gibt unzählige technische Gründe für solche – vermeidbare – Accidental Complexity.

In der Praxis ergibt sich Accidental Complexity häufig daraus, dass sich das Team für zukünftige Anforderungen wappnet. Beispielsweise stellt der Product Owner in der Pre-Production eine große Vision vor. Wir wollen Hunderte verschiedene Kunden bedienen. Sie sollen beliebig viele Integrationsmöglichkeiten in unser System haben.

Diese Produktvision löst im Entwicklungsteam den Wunsch aus, sich vorzubereiten. Um Hunderte von Kunden zu bedienen, ist es notwendig, dass wir die Userdaten auf mehrere Datenbanken aufteilen. Die schier unendlichen Integrationsmöglichkeiten lassen die Idee aufkommen, eine (Micro-)Servicearchitektur umzusetzen.

In Windeseile wird ein komplexes System modelliert, das teuer in der Umsetzung und Wartung ist. Oft wird diese Vision so niemals umgesetzt. Nach zwei Jahren Entwicklungszeit stellen wir fest, dass in unserem Produkt nur eine einzige Integrationsmöglichkeit implementiert ist und weitere vom Markt überhaupt nicht gefragt sind. Und dennoch investieren wir Monate, um eine serviceorientierte Architektur zu entwickeln – ein System, das auch noch weiteren Aufwand bei der Pflege mit sich bringt.

Wie teuer ist Accidental Complexity?

Wie teuer die Umsetzung werden kann, wird deutlich, wenn wir die Konsequenzen betrachten. Dazu ein Beispiel: Ein Integrationsservice könnte dem Kunden die Möglichkeit geben, über neue Produkte in der Plattform per Webhook informiert zu werden.

Diese API wird wie oben beschrieben als separater Service in unserer Architektur entwickelt. Wenn wir nun in unserem System ein neues Produkt einstellen, muss unser Webhook-Service die notwendigen Daten bekommen.

Es gibt selbstverständlich mehrere Möglichkeiten, das umzusetzen. Wenn wir uns vorstellen, dass das Team sich auf sehr viele mögliche Integrationsservices eingestellt hat, wurde unter Umständen eine Message Queue als Middleware installiert. In unserem System befinden sich also drei Komponenten.

Eine Message Queue ist aus Entwicklersicht schnell aufgesetzt. In der Entwicklung wird das Docker-Compose File um einen Service erweitert. Eine Library zum Message Handling wird zu der Hauptapplikation hinzugefügt. Die Logik zum Verpacken der notwendigen Daten ist schnell geschrieben. Das wiederholt das Team auf der Seite des Integrationsservices und die oberflächlichen Anforderungen sind abgeschlossen.

Sobald das in die Produktion gehen soll, fallen weitere Anforderungen auf. Die Provisionierung des Services in der Produktions- und Testumgebung muss umgesetzt werden. In vielen Firmen erfordert das die Zusammenarbeit mit einer externen Systemadministration.

Dieser Service fällt unter das Service Level Agreement (SLA) des Produkts. Damit muss auch ein Monitoring und Alerting für die Message Queue entwickelt werden. Selbst wenn das Wissen über die relevanten Metriken zur Sicherstellung des Betriebs vorhanden ist, müssen weitere Tage bis Wochen in die Entwicklung investiert werden.

Die Metriken müssen im Deployment erfasst werden, zum bereits vorhandenen Monitoring Stack geleitet und in diesem sinnvoll aufbereitet werden. Die Alerting-Regeln für den neuen Dienst müssen erstellt und getestet werden.

Nach kurzer Zeit fällt auf, dass ähnlich wie bei der Datenbank Änderungen am System vorkommen, die es erfordern, vorhandene Einstellungen des Brokers zu migrieren. Manche Änderungen können in einem Blue-Green Deployment vorgenommen werden, wie die Einführung neuer Queues; andere bilden Breaking Changes ab, die ohne Downtime nicht vorgenommen werden können.

Da diese Änderungen und die korrekte Ausführung am Tag des Deployments ebenfalls automatisiert werden müssen, investieren wir weitere Zeit in die Konzeption einer Migrationsstrategie und in die Versionierung unserer Message Queues.

Nach und nach entdeckt das Team Lücken in der Provisionierung seiner Lösung. Der zugeteilte Arbeitsspeicher ist nicht ausreichend. Die Queue Size ist in manchen Szenarien nicht groß genug. Der Zugriff von Entwicklern zum Debugging ist nicht vorhanden. Die mitgelieferte Admin-Oberfläche wird nicht nach außen freigegeben.

Auch das Bereitstellen von End-to-End Tests erweist sich als komplexer, denn die Tests müssen ebenfalls die Message Queue hochfahren. Dadurch ergibt sich eine deutlich verlängerte Laufzeit. Um das zu mitigieren, ist eine dynamische Port-Zuweisung für eine parallele Ausführung notwendig.

Diese Auflistung an zusätzlichen Aufwendungen ist mit Sicherheit nicht vollständig, doch macht sie deutlich, welche Tragweite die Einführung einer weiteren Systemkomponente mit sich bringt. All die aufgeführten Beispiele gelten ebenso für den separaten Integrationsservice.

Wo stehen wir nun also? Wir haben am Markt gelernt, dass die anfängliche Vision des Produkts nicht eingetreten ist. Wir haben lediglich eine einzige Integration gebaut. Doch die Komplexität unseres Systems ist immens.

Und diese entstandene Komplexität muss vom Team bei jeder Änderung geschultert werden. Das führt dazu, dass vermeintlich einfache Anforderungen eine lange Entwicklungszeit benötigen. Was wiederum zur Folge hat, dass mehr Mitarbeiter gebraucht werden, um in gleicher Geschwindigkeit äquivalente Fortschritte zu erzielen.

Hinzu kommt der zusätzliche Aufwand für die Einarbeitung neuer Mitarbeiter. Jedes Teammitglied muss den beschriebenen Aufbau verstehen und nachvollziehen.

Und natürlich sind mit dieser Architektur höhere Betriebskosten zu erwarten. Letztlich hätte die gesamte Funktionalität in einer einzigen Anwendung als Deployment-Monolith abgebildet werden können.

Accidental Complexity verhindern - einige Tipps

Wie so oft in der Softwareentwicklung gibt es kein Patentrezept. Der Rat, immer mit einer monolithischen Anwendung zu beginnen, kann in den meisten Fällen zwar gut sein, aber in manchen Szenarien auch nicht.

Auch die Art und Weise, wie sich Accidental Complexity im System manifestiert, kann sich stark unterscheiden. Vielleicht wurde zu Beginn auf eine reaktive API gesetzt, wobei das klassische Thread-per-Request-Modell alle Anforderungen erfüllt hätte.

Oder es wurde eine NoSQL-Datenbanklösung genutzt, wobei in der Praxis nur relationale Datensätze vorliegen.

Die Gründe für Accidental Complexity im System sind vielschichtig. Heutige Accidental Complexity mag zu einem früheren Zeitpunkt aufgrund anderer Anforderungen nicht als solche betrachten worden sein.

Ein anderer Grund ist das viel beschriebene Overengineering oder Gold Plating. Ein Entwickler setzt über die Anforderungen hinaus, in vermeintlicher Voraussicht, (technische) Features um. Das geschieht aus der guten Absicht heraus, für zukünftige Situationen gewappnet zu sein. In der Praxis werden viele dieser Features aber nie benötigt. Was bleibt, ist ein komplexeres System.

Es ist wichtig, dass das Team regelmäßig einen Schritt zurücktritt. Betrachte deine Lösung von Neuem. Stelle dir die Frage: Wie würde ich mit dem heutigen Wissen mein System designen? Was würde ich anders machen? Welche Komponenten würde ich weglassen? Welche hinzufügen?

Vielleicht war es nicht die richtige Entscheidung, das CRM als eigenen Service zu deployen. Oder kannst du deinen externen Autorisierungsservice durch ein simpleres System innerhalb der Bestandslösung ablösen? Sind die vom Standard abweichenden Prozesse in deinem Deployment wirklich notwendig oder kannst du auf eine Standardimplementierung wechseln?

Mit dem Blick eines Unwissenden, der die Historie des Projekts nicht kennt, deckt man die Komplexität auf, die nicht unmittelbar für deine Lösung notwendig ist.

Und wenn du sie entdeckt hast, sei mutig und baue deine Lösung zurück. Denn mit jeder weiteren technischen Schuld wird der Umbau aufwendiger.

Kennst du schon Marcus' Backend Newsletter?

Neue Ideen. 2x pro Woche!
Top