Fitness Functions: Architektur, die man messen kann
Architekturregeln, die man nur in Reviews erhofft, verfallen. Fitness Functions machen Eigenschaften wie Kopplung, Abhängigkeiten und Latenz kontinuierlich prüfbar – und verkürzen den Weg von der Entscheidung zur belastbaren Gewissheit.
In fast jedem Architektur-Review, das ich begleite, gibt es diesen einen Satz: "Die Domäne darf natürlich nicht auf die Infrastruktur zugreifen." Alle nicken. Es wird ein Diagramm gezeichnet, vielleicht ein Confluence-Eintrag angelegt. Und drei Sprints später zeigt eine Import-Anweisung von der Domain-Schicht direkt in den Datenbank-Adapter. Niemand hat es böswillig getan. Es war Freitagnachmittag, der Bugfix musste raus, und die Regel stand nun mal nicht im Code, sondern in einem Protokoll, das keiner mehr liest.
Genau hier setzt der Gedanke der Fitness Functions an. Die These ist unbequem einfach: Eine Architektureigenschaft, die du nicht automatisch prüfst, hast du nicht. Du hoffst sie. Und Hoffnung ist keine Architektur.
Vom Wunsch zur mechanischen Prüfung
Der Begriff stammt aus Building Evolutionary Architectures von Neal Ford, Rebecca Parsons und Patrick Kua (O'Reilly, 2017), entliehen aus dem Evolutionary Computing, wo eine Fitness-Funktion misst, wie nah eine Lösung am Ziel ist. Übertragen auf Software heißt das: Eine architectural fitness function ist ein objektiver, mechanischer Integritätstest für ein oder mehrere architectural characteristics – also für die Eigenschaften, die man früher etwas sperrig "-ilities" nannte: Performance, Skalierbarkeit, Kopplung, Sicherheit.
Der Perspektivwechsel, der in der Praxis den Unterschied macht, ist folgender: Architektur wird nicht länger als Zustand behandelt, den man einmal beschließt, sondern als Eigenschaft, die man kontinuierlich einhält. Das klingt nach einer Nuance, ist aber ein Bruch im Arbeitsmodus. Ein Diagramm beschreibt, wie das System aussehen soll. Eine Fitness Function beschreibt, wie es aussehen muss – und bricht den Build, wenn es das nicht tut.
Damit verschiebt sich Governance von der Sitzung in die Pipeline. Statt eines Architecture Review Boards, das quartalsweise tagt und Abweichungen im Nachhinein entdeckt, läuft die Prüfung bei jedem Commit mit. Der Business-Outcome ist der eigentliche Punkt und nicht die Technik: kürzere Feedback-Zyklen, weniger Reibung zwischen Teams und Architektur, und eine Qualität, die nicht von der Tagesform des Reviewers abhängt. Wer schon einmal erlebt hat, wie ein einziger schleichender Kopplungsverstoß über Monate ein Refactoring blockiert, kennt den Preis der nachgelagerten Kontrolle.
Die erste Fitness Function ist ein Unit-Test
Der Einstieg ist unspektakulär, und das ist die gute Nachricht. Die einfachste Kategorie sind statische, atomare Fitness Functions – Regeln über die Struktur des Codes, die sich als ganz normaler Test ausdrücken lassen. In der Java-Welt hat sich dafür ArchUnit von TNG etabliert, das über Bytecode-Analyse Paket- und Schichtabhängigkeiten prüft. Für .NET gibt es NetArchTest, im JavaScript-Umfeld den Dependency-Cruiser. Das Prinzip ist überall dasselbe.
Die eingangs beschworene Regel – Domäne kennt keine Infrastruktur – sieht als Fitness Function so aus:
@ArchTest
static final ArchRule domain_must_not_depend_on_infrastructure =
noClasses().that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAPackage("..infrastructure..");
@ArchTest
static final ArchRule no_cyclic_dependencies =
slices().matching("..(*)..").should().beFreeOfCycles();
Das Schöne daran: Diese Regel ist selbst dokumentierend. Sie steht im Repository, sie läuft in der CI, und sie ist nicht verhandelbar, ohne dass jemand sie sichtbar ändert. Der Confluence-Eintrag von oben war eine Meinung. Dieser Test ist eine Tatsache.
Der zweite Block – no_cyclic_dependencies – adressiert einen Klassiker, der in Reviews notorisch schwer zu erkennen ist. Zyklische Abhängigkeiten entstehen selten in einem großen Schritt, sondern durch viele kleine, je für sich harmlose Importe. Ein Mensch sieht den Zyklus im Diagramm nicht mehr, wenn er über sieben Module läuft. Die Maschine schon. Genau solche Eigenschaften – afferent/efferent coupling, Instabilität, Zyklenfreiheit – sind der Sweet Spot für statische Fitness Functions, weil sie deterministisch und billig zu prüfen sind.
Eine Landkarte statt einer Werkzeugliste
Wer bei Fitness Functions nur an ArchUnit denkt, verkennt die Reichweite des Konzepts. Ford, Parsons und Kua schlagen eine Taxonomie vor, die ich in Beratungsprojekten gern als Sortierhilfe nutze, weil sie hilft, den richtigen Testtyp für die jeweilige Eigenschaft zu wählen. Zwei Achsen genügen für den Anfang:
quadrantChart
title Einordnung von Fitness Functions
x-axis "atomic (ein Merkmal)" --> "holistic (Zusammenspiel)"
y-axis "triggered (bei Bedarf)" --> "continuous (laufend)"
quadrant-1 "Latenz-/Lasttests in Prod"
quadrant-2 "Chaos-/Resilienz-Checks"
quadrant-3 "ArchUnit-Dependency-Rules"
quadrant-4 "Security-Scan pro Commit"
Atomic misst eine einzelne Eigenschaft isoliert, holistic prüft das Zusammenspiel mehrerer – etwa Sicherheit und Latenz gemeinsam unter Last. Triggered läuft auf Anstoß, etwa im Build, continuous läuft dauerhaft gegen das produktive System. Dazu kommen weitere Dimensionen aus dem Buch: static versus dynamic, automated versus manual, sowie temporal fitness functions, die bewusst nur für eine begrenzte Zeit gelten – etwa eine Erinnerung, eine Bibliotheksmigration bis zu einem Stichtag abzuschließen.
Der praktische Wert dieser Landkarte liegt nicht in der Klassifikation um ihrer selbst willen. Er liegt darin, dass sie eine unbequeme Frage erzwingt: An welches konkrete architectural characteristic ist diese Prüfung eigentlich gekoppelt? Wer das nicht beantworten kann, baut vermutlich gerade eine Vanity-Metrik.
Wenn die Eigenschaft dynamisch ist
Nicht alles lässt sich statisch prüfen. Latenz zum Beispiel ist keine Eigenschaft des Quelltexts, sondern des laufenden Systems. Hier kommen dynamische Fitness Functions ins Spiel, die gegen ein Deployment messen. Konzeptionell bleibt es dasselbe – ein Schwellwert, ein Vergleich, ein Urteil:
def latency_fitness(service):
p95 = measure_p95_ms(service, window="5m")
assert p95 < LATENCY_BUDGET_MS, f"p95 {p95} exceeds budget"
So sauber das aussieht, so gefährlich ist es, diesen Assert blind als hartes Gate in die CI zu hängen. Latenzwerte schwanken. Ein p95 über ein Fünf-Minuten-Fenster kann durch einen einzelnen Garbage-Collection-Peak oder einen langsamen Nachbarn im Cluster reißen, ohne dass sich an deiner Software irgendetwas geändert hätte. Wer das ignoriert, produziert flaky builds – und ein flaky gate wird binnen zwei Wochen weggeklickt oder auskommentiert. Dann hast du die Reibung der Prüfung, aber nicht mehr ihren Nutzen.
Die pragmatische Antwort ist eine Trennung: Statische, deterministische Eigenschaften dürfen den Build blockieren. Dynamische, statistisch schwankende Eigenschaften laufen als beobachtende, getriggerte Checks mit statistischen Fenstern und alarmieren, statt hart zu brechen. Dieselbe Idee, zwei Betriebsmodi – je nachdem, wie deterministisch das gemessene Merkmal ist.
Für resilienzbezogene Eigenschaften lohnt der Blick in Michael Nygards Release It!. Die dort beschriebenen Stability Patterns – Circuit Breaker, Bulkhead, Timeout – liefern nicht nur Bausteine für robuste Systeme, sondern auch prüfbare Erwartungen: Öffnet der Circuit Breaker unter simulierter Überlast wie vorgesehen? Auch das ist eine Fitness Function, nur eben eine dynamische.
Kopplung zwischen Services messen
Die interessanteste Kopplung in verteilten Systemen verläuft nicht innerhalb eines Deployables, sondern zwischen ihnen. Zwei Teams, zwei Services, ein Vertrag – und die stille Annahme, dass der andere sein API nicht kaputtmacht. Consumer-Driven Contract Testing macht diese Annahme prüfbar. Mit Pact und einem Pact Broker beschreibt der Konsument seine Erwartung an den Provider, und der Provider verifiziert gegen genau diese Erwartung.
Als Fitness Function landet das direkt als Schritt in der Deployment-Pipeline:
pact-broker can-i-deploy \
--pacticipant Checkout \
--version "$GIT_SHA" \
--to-environment production
Die Frage "kann ich das deployen, ohne einen Konsumenten zu brechen?" wird damit von einer Bauchentscheidung zu einer mechanischen Antwort. Das ist Kopplung als Governance-as-Code: nicht die Meinung, dass man kompatibel bleibt, sondern der Nachweis. In der Praxis ist das oft der Moment, in dem Teams zum ersten Mal wirklich unabhängig deployen können, weil die Angst vor dem unbemerkten Bruch aus dem Prozess genommen ist.
Der Ort, an dem alles zusammenläuft
Fitness Functions entfalten ihre Wirkung nicht als lose Testsammlung, sondern als Gate in der Auslieferung. Die Deployment-Pipeline – konzeptuell seit Humble und Farley etabliert – ist der natürliche Ort dafür. Zwischen Build und Deploy liegt eine Barriere, die mehrere Kategorien parallel prüft:
flowchart LR
commit["commit"] --> build["build"]
build --> gate{"Fitness<br/>Functions<br/>Gate"}
gate -->|static / atomic| s["Dependency & Cycle Rules"]
gate -->|dynamic / holistic| d["Latency p95 & Load"]
gate -->|contract| c["Pact can-i-deploy"]
s -->|fail = break build| commit
d -->|fail = break build| commit
c -->|fail = break build| commit
gate -->|all green| deploy["deploy"]
Die roten Rückpfeile sind der Kern der Idee: Ein Verstoß führt nicht zu einem Ticket, das irgendwann priorisiert wird, sondern zu einem gebrochenen Build, den derjenige repariert, der ihn verursacht hat – sofort, mit vollem Kontext, bevor der Fehler in die Historie einsintert. Das ist derselbe Mechanismus, der Continuous Integration wirksam macht, angewandt auf Architektureigenschaften.
Und hier schließt sich der Kreis zur messbaren Auslieferungsqualität. Accelerate von Forsgren, Humble und Kim und die DORA Four Key Metrics – Deployment Frequency, Lead Time for Changes, Change Failure Rate, Time to Restore – liefern die Outcome-Ebene. Fitness Functions sind das architektonische Pendant dazu: Sie sichern pipeline-nah genau die Eigenschaften ab, die verhindern, dass diese Metriken unter Wachstum abrutschen. Ein System mit sauberer, geprüfter Kopplung lässt sich häufiger und angstfreier deployen. Das ist kein Zufall, das ist Kausalität.
Wo es in der Praxis kippt
So überzeugend das Konzept ist, so zuverlässig scheitert seine naive Umsetzung an drei Stellen – und die habe ich in Projekten alle gesehen.
Erstens die Vanity-Metrik. Eine grüne Fitness Function, die eine reine Test-Coverage-Schwelle prüft, fühlt sich nach Sicherheit an und misst doch nichts Architektonisches. Coverage sagt, wie viel Code ausgeführt wird, nicht, ob die Abhängigkeiten stimmen. Jede Function muss an ein konkretes architectural characteristic gekoppelt sein, sonst ist sie Theater.
Zweitens die Wartungslast. Fitness Functions sind Code und altern wie Code. Ich habe Codebasen gesehen, in denen jede zweite Regel ein @ArchIgnore trug, weil es unter Termindruck schneller ging, die Ausnahme einzutragen, als das Problem zu lösen. Ohne Ownership und regelmäßiges Aufräumen der Ausnahmeliste erodiert die Aussagekraft, bis nur noch grünes Rauschen bleibt. Diese Regeln brauchen dieselbe Pflege wie Produktionscode – sonst sind sie schlimmer als nichts, weil sie falsche Sicherheit erzeugen.
Drittens der zu späte Einstieg. Wer Fitness Functions auf eine gewachsene Legacy-Codebasis loslässt, bekommt beim ersten Lauf Tausende Violations – und garantiert Frust. Der Ausweg ist eine Baseline: Man friert den Ist-Zustand ein, deklariert Fitness von jetzt an und lässt nur noch Verbesserung zu, keine Verschlechterung. "Declare fitness from now on" ist kein Betrug an der Reinheit, sondern die einzige Art, wie so etwas in echten Systemen überhaupt startet.
Fazit
Der rote Faden dieser drei Bausteine – statische Regeln, dynamische Messungen, Vertragsprüfungen – ist ein einziger Gedanke: Architektur ist eine Eigenschaft, die man einhält, keine Entscheidung, die man einmal trifft. Fitness Functions verschieben die Kontrolle vom Review in die Pipeline und damit von der Hoffnung zur Gewissheit.
Der Gewinn ist selten die einzelne Regel. Er liegt im veränderten Arbeitsmodus. Teams, die ihre wichtigsten Architektureigenschaften mechanisch absichern, diskutieren in Reviews nicht mehr über Importrichtungen, sondern über Fachlichkeit. Sie deployen öfter, weil die stille Angst vor dem unbemerkten Bruch aus dem Prozess genommen ist. Und sie refaktorieren mutiger, weil ein Netz aus grünen Checks sofort meldet, wenn eine Umstrukturierung eine Eigenschaft verletzt.
Mein Rat aus der Praxis ist bewusst bescheiden: Fang mit einer einzigen atomaren, statischen Regel an, die eine Eigenschaft absichert, die dir wirklich wichtig ist. Eine Abhängigkeitsregel, die den Build bricht, ist mehr wert als zwanzig Regeln in einem Dokument, das keiner liest. Der Rest wächst von dort – einfach und elegant, nicht als Prozess-Overhead, sondern als Code, der einfach mitläuft.
Weiterführende Quellen
- Neal Ford, Rebecca Parsons, Patrick Kua: Building Evolutionary Architectures (O'Reilly, 2017) – Kapitel zu Fitness Functions
- ArchUnit (TNG) – statische Architekturtests für Java
- Nicole Forsgren, Jez Humble, Gene Kim: Accelerate (2018) und die DORA Four Key Metrics – martinfowler.com: StateOfDevOpsReport
- Michael Nygard: Release It! (2. Auflage, 2018) – Stability Patterns für resilienzbezogene Fitness Functions
- Pact – Consumer-Driven Contract Testing mit
can-i-deploy
Kommentare