Teamwork
Vier kleine Entscheidungen, die uns teuer wurden
Nach mehr als einem Jahrzehnt Betrieb derselben Anwendung bleibt eine Liste. Entscheidungen, die in dem Moment plausibel wirkten, sich später als teuer herausstellten und sich heute als kurze Lehrsätze formulieren lassen. Im Folgenden vier davon — keine Heldengeschichten, sondern Vorfälle, die weh getan haben und die jeder erkennen wird, der lange genug Software betreibt.
Was diese Geschichten verbindet, ist nicht eine technische Disziplin, sondern ein Muster: Eine kleine Abkürzung, die im Moment vernünftig schien, hat sich später als ungleich teurer herausgestellt als der Umweg, den wir uns gespart hatten.
1. Eine App nicht in derselben App neu schreiben
Wir wollten einen größeren Teil unserer Rails-Anwendung umbauen — neue Architektur, derselbe Codebase, klassischer In-Place-Rewrite. Während der Migration deployten wir Änderungen auf Produktion in der Annahme, im Notfall per Git oder Release-Rollback wieder herauszukommen. Was wir übersehen hatten: Wir hatten den Mechanismus, der vorherige Releases auf dem Server vorhält, zur Aufräumung deaktiviert. Als ein Deployment brach, gab es kein Release, auf das wir zurückrollen konnten.
Seitdem schreiben wir größere Umbauten nie wieder in derselben Anwendung. Eine neue Version wird parallel unter einer Subdomain aufgebaut, und der Wechsel passiert erst, wenn das neue Setup nachweislich trägt. Die alte Version bleibt erreichbar, bis die neue eine Weile sauber gelaufen ist. Das kostet Initial-Aufwand und spart im Ernstfall einen Tag, an dem niemand sinnvoll arbeitet, weil alle auf den nächsten Restore-Versuch warten.
2. Validierungen gehören in den Prozess, nicht ins Modell
Über Jahre wuchsen die Validierungen in unseren ActiveRecord-Modellen — jede Anforderung aus jedem Geschäftsprozess landete dort, weil es der schnellste Weg war. Bis irgendwann jemand schlicht eine Adresse im Customer-Modell aktualisieren wollte und das Speichern fehlschlug. Die Ursache: Der Kunde hatte zuvor einen „Passwort vergessen”-Prozess angestoßen, was im Modell eine Validierung scharfgeschaltet hatte, die für jede beliebige Speicherung zwingend ein neues Passwort verlangte. Stunden für die Diagnose, weil niemand die gewachsene Validierungs-Kette mehr im Kopf hatte.
Seitdem schreiben wir dezidierte Prozesse als Tableless-Klassen oder Service-Objekte. Diese halten die Validierungen, die nur in genau diesem Kontext gelten — Registrierung, Passwort-Reset, Bestellabschluss. Erst wenn die Validierung in der Prozess-Klasse durchläuft, wird das Modell gespeichert. Der Vorteil ist nicht primär Eleganz, sondern Zuordnung: Bei einem Fehler weiß man sofort, welcher Prozess hängt, statt im Modell zwischen zwei Dutzend Validierungen zu wühlen, von denen die Hälfte aus einem Sub-Workflow stammt, der gerade gar nicht relevant ist.
3. Methoden mit der eigenen Dummheit im Blick schreiben
Methoden wie diese sind gefährlich:
def calculate_invoice_total(order)
if order.present?
if order.items.any?
if order.customer.active?
# ... eigentliche Logik
end
end
end
end
Was gibt sie zurück, wenn eine der Bedingungen nicht greift? nil? false? Implizit der letzte ausgewertete Ausdruck? Niemand weiß es zuverlässig, am wenigsten der eigene Aufrufer in sechs Monaten. Wir haben uns angewöhnt, Methoden mit Guard Clauses zu öffnen — frühe Returns mit definierten Werten, wenn die Voraussetzungen nicht stimmen.
def calculate_invoice_total(order)
return 0 if order.nil?
return 0 if order.items.empty?
return 0 unless order.customer.active?
# ... eigentliche Logik
end
Der Effekt ist kleiner, als er klingt, und größer, als man denkt. Code wird lesbarer, weil die Hauptlogik nicht mehr drei Einrückungstiefen tief liegt. Vor allem aber: Aufrufer wissen, womit sie rechnen müssen. Aus „kommt vielleicht irgendwas, vielleicht nil” wird „kommt definiert eine Zahl, im Zweifel Null”. Das eliminiert eine ganze Klasse von Bugs, in denen Aufrufer auf eine Annahme bauen, die in einem Edge Case nicht stimmt.
4. Einen Scope nie ändern, ohne zu wissen, wo er benutzt wird
Ein zentraler Scope sammelte alle Aufträge, die für den nächsten Rechnungslauf in Frage kamen. Wir haben ihn an einer Stelle leicht angepasst — eine zusätzliche Bedingung, die für ein neues Feature relevant war. Was wir vor der Änderung nicht geprüft hatten: dass derselbe Scope an mehreren anderen Stellen in der Anwendung genutzt wurde, darunter im nächtlichen Rechnungslauf.
Am nächsten Morgen lief der Job mit einer falschen Auswahl. Zwei Tage manueller Aufräumarbeit, Aufträge per Datenbank-Eingriff in den richtigen Status zurücksetzen, Rechnungen prüfen und anpassen. Seitdem ist die Regel klar: Vor jeder Änderung an einem Scope, einer Helper-Methode oder einer zentralen Konfigurationsoption schauen wir, wo sie überall referenziert wird. Bei einer Anwendung, die seit über zehn Jahren wächst, ist die Antwort selten die, die man im Kopf hat — und der Befehl grep ist erstaunlich oft die nüchterne Lebensversicherung.
Was diese Liste verbindet
Vier Vorfälle, vier kleine Optimierungen, die jeweils im Moment vernünftig wirkten: den Aufräum-Mechanismus deaktivieren, eine Validierung schnell ins Modell packen, eine geschachtelte Bedingung statt einer Guard Clause, einen Scope mal eben anpassen. Keine davon war fahrlässig im klassischen Sinn, jede hat sich später teurer rückbezahlt, als sie eingespart hat.
Daraus folgt keine spezifische Lehre über Rails oder Software, sondern eine über die Art, wie man Bestand baut. Wer eine Anwendung über Jahre betreibt, lernt, dass Vorsicht im Schnitt billiger ist als Eile. Diese Liste gibt es nicht, weil wir besonders fehlerhaft wären — sondern weil wir lange genug betreiben, dass solche Listen überhaupt entstehen.