Warum Architekturentscheidungen viel schwieriger erscheinen, als sie sind
Vor ein paar Jahren verbrachte ich drei Abende damit, eine scheinbar „einfache“ Funktion in OpenClaw hinzuzufügen: einen projektbezogenen Rate Limiter für Webhooks. Drei Abende. Nichts, was ich versuchte, fügte sich sauber ein. Jede Änderung betraf fünf Dateien. Jedes „Fix“ brach irgendeine Integration, an die wir seit 2021 halbwegs vergessen hatten.
Das war der Moment, in dem ich merkte, dass unser Problem nicht fehlende Tests oder faules Refactoring war. Es war, dass wir nicht ausreichend bedacht hatten, wie Architekturentscheidungen getroffen werden sollten. Wir hatten Code, der funktionierte, aber er mochte keine Veränderungen.
Wenn du zu OpenClaw beiträgst (oder darüber nachdenkst), möchte ich dir zeigen, wie wir tatsächlich Architekturentscheidungen treffen. Nicht die idealisierte „wir haben ein Box-Diagramm gezeichnet“-Version. Die Realität: die Abwägungen, das „wir werden es bereuen, aber es ist es wert“ und die Stellen, an denen wir absichtlich die Dinge langweilig halten.
Die eigentliche Aufgabe der Architektur: Veränderungen günstig machen
Mir ist egal, wie hübsch die Diagramme sind. Wenn eine Änderung dich ein ganzes Wochenende und eine Migräne kostet, lügt dir deine Architektur ins Gesicht.
In OpenClaw haben wir begonnen, einen sehr einfachen Test für Architekturentscheidungen zu verwenden:
- Macht dies die nächste Änderung günstiger?
- Macht dies das Debuggen schneller als es heute ist?
Das war’s. Nicht „ist dieses Muster korrekt“ oder „wird das auf 10 Millionen Nutzer skalieren“. Wir haben keine 10 Millionen Nutzer. Wir haben aber Maintainer, die ausbrennen, wenn das Hinzufügen einer kleinen Funktion bedeutet, 2.000 Zeilen unzusammenhängenden Codes lesen zu müssen.
Beispiel: Im August 2024 führten wir die Abstraktion ExecutionPlan im Job-Runner ein. Davor bedeutete das Hinzufügen eines neuen Ausführungsschrittes:
- Ändern des Kern-Schedulers (schlechte Idee #1)
- Bearbeiten von 3 verschiedenen Enums (schlechte Idee #2)
- Aktualisieren von zwei Stellen, die SQL-Abfragen manuell erstellten (schlechte Idee #3)
Wir haben den Mut gefasst und ExecutionPlan als separates Modul erstellt. Ja, es war ein großer Diff (ca. 1.200 Zeilen geändert). Ja, es brach eine Menge interner Skripte. Aber jetzt ist es eine Datei zu verstehen, ein Ort, um einen neuen Schritt einzufügen, und der Scheduler muss sich nicht um die Details kümmern.
War es „perfekte Architektur“? Definitiv nicht. Wir haben bereits Teile davon zweimal überarbeitet. Aber jede Änderung seitdem war kleiner, sicherer und einfacher zu überprüfen. Das ist die einzige Kennzahl, auf die ich wirklich schaue.
Wie wir entscheiden, wo eine Funktion hingehört (und wann wir nein sagen)
Architektur in Open Source ist besonders eigenartig, weil man nicht nur gegen Komplexität kämpft, sondern auch gegen Erwartungen. Funktionsanfragen kommen mit starken Meinungen darüber, wo die Dinge „leben sollten“.
Daher stützen wir uns in OpenClaw auf drei einfache Fragen, wenn wir entscheiden, wo eine neue Funktion hingehört:
- Was besitzt tatsächlich die Daten? (Code sollte nahe bei seinen Daten leben)
- Wer debuggt das, wenn es kaputt geht? (vor Augen halten, welches mentale Modell diese Person hat)
- Kann jemand das in einem Jahr löschen, ohne das gesamte Repo lesen zu müssen?
Lass mich dir ein konkretes Beispiel zeigen.
Im März 2025 öffnete jemand ein Ticket und bat um „inline Lua-Skripting innerhalb der Pipeline-Definitionen“. Sehr coole Idee. Sehr gefährlich für die Codebasis. Es gab drei offensichtliche Möglichkeiten, dies zu tun:
- Inline-Skripte im YAML-Parser (verlockend, aber verflucht)
- Eine Skripting-Schicht im Kern-Engine hinzufügen (sehr verflucht)
- Skripte als Plugins mit einer engen Schnittstelle behandeln (langweilig, mehr Arbeit)
Hätten wir es im YAML-Parser integriert, wäre es schneller ausgeliefert worden, aber jede kleine Änderung der Konfigurationssyntax hätte das Risiko, dass Kunden-Skripte auf merkwürdige Weise brechen. Das ist eine Zeitbombe für zukünftige Maintainer.
Wir haben uns für die pluginartige Schnittstelle entschieden, die über der Pipeline-Ausführungs-Engine liegt. Das bedeutete:
- Ein kleines Lua-Runtime-Modul, das keinerlei Wissen über YAML hat
- Eine klare Grenze:
Config → Engine → ScriptAdapter - Zwei Konfigurationsflags, um die Funktion in Deployments, die sie nicht wollen, abzuschalten
Es hat länger gedauert. Der ursprüngliche PR war #1897, der am 9. April 2025 nach vier Durchgängen der Überprüfung zusammengeführt wurde. Aber jetzt, wenn jemand „inline JS“ oder „inline WASM“ hinzufügen möchte, gibt es einen sehr offensichtlichen Ort, um das zu integrieren. Wir haben einmal für eine saubere Naht bezahlt; wir dürfen sie immer wieder verwenden.
Nein zu sagen ist auch eine Architekturentscheidung. Wir haben Tickets mit „das gehört in einen Sidecar-Dienst, nicht in OpenClaw“ mehr als einmal geschlossen. Das ist nicht unsere Sturheit; das schützt das Kernsystem, damit es verständlich bleibt.
Muster, die wir absichtlich verwenden (und die wir vermeiden)
Es gibt ein Museum von Mustern, die man in ein Projekt einfügen kann. Die meisten gehören nicht zu OpenClaw.
Die Muster, auf die wir tatsächlich immer wieder setzen:
- Ports und Adapter für Integrationen und IO
- Ereignisgesteuerte interne Abläufe, wo wir wissen, dass wir später mehr Listener hinzufügen werden
- „Konfiguration an den Rändern“ mit typisiertem Code in der Mitte
Ports/Adapter tauchen jetzt überall in unserer Codebasis auf:
- Speicher:
StoragePortmit Postgres-, S3- und Dateisystemadaptern - Messaging:
QueuePortmit Redis- und NATS-Adaptern - Auth:
AuthPortmit OIDC- und statischen Token-Adaptern
Der Vorteil ist einfach: Als jemand Ende 2025 kam und eine MinIO-Unterstützung wollte, war es ein ~180-zeiliger Adapter anstatt „jede Aufrufstelle, die auf Speicher zugreift, neu zu schreiben.“ Das ist die Art von Abwägung, die ich gerne eingehe.
Dinge, die wir weitgehend vermeiden:
- Tiefe Vererbungshierarchien. Wir bevorzugen Daten + Funktionen.
- Übermäßig generische Abstraktionen. Wenn der generische Name schwerer zu verstehen ist als „PostgresStorage“, haben wir es zu clever gemacht.
- Globale Singletons. Konfiguration und Dienste werden die meiste Zeit explizit übergeben. Es ist etwas nervig, aber das Debuggen ist viel einfacher.
Ein spezifisches Beispiel für „zu clever“, das wir entfernt haben: der alte „UniversalBackendManager“ aus Anfang 2023. Er bündelte:
- Speicherung
- Warteschlange
- Authentifizierung
- Caching
Alles hinter einer Mega-Schnittstelle. Zuerst sah es gut aus. Dann versuchten wir, nur die Cache-Implementierung zu ändern. Das erforderte das Bearbeiten des Managers, der DI-Verkabelung und der Hälfte der Tests. Mitte 2024 haben wir es gelöscht und durch vier kleine Schnittstellen ersetzt. Mehr Boilerplate, besseres Leben.
Wie wir Entscheidungen festhalten (ohne im Prozess zu ertrinken)
Architekturdokumente können schneller verrotten als der Code, den sie beschreiben. Deshalb halten wir es absichtlich leichtgewichtig. Du wirst drei Hauptdinge im OpenClaw-Repo sehen:
- ADRs (Architecture Decision Records) in
docs/adr/ - „Warum“-Kommentare über seltsam aussehenden Code-Pfaden
- PR-Beschreibungen, die über Abwägungen sprechen, nicht nur über „was geändert wurde“
Unsere ADRs sind kurz. Die für den neuen Scheduler (ADR-0007, datiert 2024-11-02) lautet im Wesentlichen:
- Kontext: alter Scheduler war zu stark an die HTTP-Schicht gebunden
- Entscheidung: Planung in ein separates Dienstmodul auslagern
- Alternativen: So belassen oder ganz in eine externe Warteschlange verschieben
- Folgen: etwas mehr Konfiguration, aber bessere Isolation und einfacheres Skalieren
Das sind etwa eine Seite Text. Du kannst es in weniger als zwei Minuten lesen. Aber wenn ein neuer Mitwirkender einsteigt und fragt: „Warum ist der Scheduler eine eigene Sache?“, haben wir eine Antwort, die nicht nur „weil Kai es wollte“ lautet.
Gleiches gilt für PRs: Wenn du etwas Architektonisches änderst, möchten wir wirklich sehen:
- Was du in Betracht gezogen, aber nicht getan hast
- Was einfacher oder schwieriger sein wird, nachdem dies umgesetzt wurde
- Alle „seltsamen“ Entscheidungen, die du absichtlich getroffen hast
Du brauchst kein 10-seitiges Entwurfsdokument. Ein paar ehrliche Absätze reichen aus.
FAQ
F: Ich bin ein neuer Beitragender. Wie vermeide ich es, eine „schlechte“ Architekturänderung vorzunehmen?
Fang klein an und mache laut auf dich aufmerksam. Öffne einen Entwurf-PR oder eine GitHub-Diskussion mit deiner Idee, bevor du zu viele Dateien berührst. Zeige einen kleinen Prototyp, erkläre die Abwägungen, die du siehst, und frage: „Wo würde das hingehören?“ Du wirst schneller Feedback erhalten, und du wirst kein Wochenende damit verbringen, etwas zu bauen, was wir ohnehin vorschlagen werden zu verschieben.
F: Ist es in Ordnung, eine neue Abhängigkeit oder einen neuen Dienst hinzuzufügen, um eine Funktion zu unterstützen?
Ja, aber wir sind wählerisch. Neue Abhängigkeiten sollten entweder die Dinge dramatisch vereinfachen oder etwas implementieren, das wir absolut nicht selbst machen sollten (Krypto, ernsthaftes Parsen usw.). Neue Dienste sind in Ordnung, wenn sie eine klare API-Grenze haben und nicht heimlich von den Interna von OpenClaw abhängen. Wenn es sich wie „nur ein weiterer Helfer“ anfühlt, gehört es wahrscheinlich besser in die bestehenden Module.
F: Muss ich für jede nicht triviale Änderung ein ADR schreiben?
Nein. ADRs sind für Entscheidungen gedacht, die verändern, wie die Leute über den Code denken: neue Module, neue schneidende Muster, die Stilllegung alter Ansätze. Wenn du internen Code umsortierst, aber das mentale Modell gleich bleibt, ist eine solide PR-Beschreibung ausreichend. Wenn du dir unsicher bist, frag im #dev-architecture-Channel, und wir sagen dir, ob es ein ADR braucht.
🕒 Published: