Why architecture decisions feel way harder than they look
A couple years ago, I spent three evenings trying to add what looked like a âsimpleâ feature to OpenClaw: a per-project rate limiter for webhooks. Three evenings. Nothing I tried slotted in cleanly. Every change touched five files. Every âfixâ broke some integration weâd half-forgotten about from 2021.
That was the moment I realized our problem wasnât missing tests or lazy refactors. It was that we hadnât been deliberate enough about architecture decisions. We had code that worked, but it didnât like being changed.
If you contribute to OpenClaw (or youâre thinking about it), I want to walk you through how we actually make architecture calls now. Not the idealized âwe drew a box diagramâ version. The real stuff: the tradeoffs, the âweâll regret this but itâs worth it,â and the places where we intentionally keep things boring.
The real job of architecture: make change cheap
I donât care how pretty the diagrams are. If making a change costs you an entire weekend and a migraine, your architecture is lying to you.
In OpenClaw, weâve started using a very simple test for architecture decisions:
- Does this make the next change cheaper?
- Does this make debugging faster than it is today?
Thatâs it. Not âis this pattern correctâ or âwill this scale to 10 million users.â We donât have 10 million users. We do have maintainers who burn out when adding a small feature means reading 2,000 lines of unrelated code.
Example: in August 2024 we introduced the ExecutionPlan abstraction in the job runner. Before that, adding a new execution step meant:
- Editing the core scheduler (bad idea #1)
- Touching 3 different enums (bad idea #2)
- Updating two places that built SQL queries by hand (bad idea #3)
We bit the bullet and created ExecutionPlan as a separate module. Yes, it was a big diff (around 1,200 lines changed). Yes, it broke a bunch of internal scripts. But now itâs one file to understand, one place to plug in a new step, and the scheduler doesnât know or care about the details.
Was it âperfect architectureâ? Definitely not. We already reworked parts of it twice. But every change since then has been smaller, safer, and easier to code review. Thatâs the only score Iâm really looking at.
How we decide where to put a feature (and when to say no)
Architecture in open source is especially weird because youâre not just fighting complexity, youâre also fighting expectations. Feature requests come in with strong opinions about where things âshouldâ live.
So in OpenClaw, we lean on three simple questions when deciding where a new feature belongs:
- What actually owns the data? (code should live close to its data)
- Who debugs this when it breaks? (keep that personâs mental model in mind)
- Can someone delete this in a year without reading the whole repo?
Let me show you a concrete example.
In March 2025, someone opened an issue asking for âinline Lua scripting inside pipeline definitions.â Very cool idea. Very dangerous for the codebase. There were three obvious ways to do it:
- Inline scripts inside the YAML parser (tempting, but cursed)
- Add a scripting layer inside the core engine (very cursed)
- Treat scripts as plugins with a tight interface (more boring, more work)
If weâd glued it into the YAML parser, it would have shipped faster, but now every little change to configuration syntax would risk breaking customer scripts in weird ways. Thatâs a time bomb for future maintainers.
We went with the plugin-style interface layered on top of the pipeline execution engine. That meant:
- A small Lua runtime module, with zero knowledge of YAML
- A clear boundary:
Config â Engine â ScriptAdapter - Two config flags to turn the feature off in deployments that donât want it
It took longer. The initial PR was #1897, merged on April 9, 2025, after four rounds of review. But now if someone wants to add âinline JSâ or âinline WASM,â thereâs a very obvious place to hook in. We paid for a clean seam once; we get to reuse it over and over.
Saying no is also an architecture decision. Weâve closed issues with âthis belongs in a sidecar service, not in OpenClawâ more than once. Thatâs not us being stubborn; thatâs protecting the core so it stays understandable.
Patterns we use on purpose (and the ones we avoid)
Thereâs a museum of patterns you can drag into a project. Most of them donât belong in OpenClaw.
The patterns we actually lean on, repeatedly:
- Ports and adapters for integrations and IO
- Event-driven internals where we know weâll add more listeners later
- âConfiguration at the edgesâ with typed code in the middle
Ports/adapters shows up everywhere in our codebase now:
- Storage:
StoragePortwith Postgres, S3, and filesystem adapters - Messaging:
QueuePortwith Redis and NATS adapters - Auth:
AuthPortwith OIDC and static token adapters
The benefit is simple: when someone came along in late 2025 and wanted MinIO support, it was a ~180-line adapter instead of ârewrite every call site that touches storage.â Thatâs the kind of tradeoff Iâll happily make.
Things we mostly avoid:
- Deep inheritance hierarchies. We favor data + functions.
- Overly generic abstractions. If the generic name is harder to understand than âPostgresStorage,â we made it too clever.
- Global singletons. Configuration and services are passed explicitly most of the time. Itâs slightly annoying, but debugging is way easier.
A specific example of âtoo cleverâ that we ripped out: the old âUniversalBackendManagerâ from early 2023. It wrapped:
- Storage
- Queue
- Auth
- Caching
All behind one mega-interface. It looked nice at first. Then we tried to change just the cache implementation. That required editing the manager, the DI wiring, and half the tests. In mid-2024 we deleted it and replaced it with four small interfaces. More boilerplate, better life.
How we record decisions (without drowning in process)
Architecture docs can rot faster than the code they describe. So we keep it intentionally lightweight. Youâll see three main things in the OpenClaw repo:
- ADRs (Architecture Decision Records) in
docs/adr/ - âWhyâ comments above weird-looking code paths
- PR descriptions that talk about tradeoffs, not just âwhat changedâ
Our ADRs are short. The one for the new scheduler (ADR-0007, dated 2024-11-02) is basically:
- Context: old scheduler too tied to HTTP layer
- Decision: pull scheduling into a separate service module
- Alternatives: keep as-is, or move to an external queue entirely
- Consequences: slightly more config, but better isolation and easier scaling
Thatâs about a page of text. You can read it in under two minutes. But when a new contributor jumps in and asks âwhy is the scheduler its own thing?â, we have an answer that isnât just âbecause Kai felt like it.â
Same for PRs: if youâre changing something architectural, we really want to see:
- What you considered but didnât do
- What will be easier or harder after this lands
- Any âweirdâ choices you made on purpose
You donât need a 10-page design doc. A few honest paragraphs are enough.
FAQ
Q: Iâm a new contributor. How do I avoid making a âbadâ architecture change?
Start small and start loud. Open a draft PR or a GitHub Discussion with your idea before you touch too many files. Show a tiny prototype, explain the tradeoffs you see, and ask âwhere would this belong?â Youâll get feedback faster, and you wonât spend a weekend building something weâre going to suggest moving anyway.
Q: Is it okay to add a new dependency or service to support a feature?
Yes, but weâre picky. New dependencies should either make things dramatically simpler or implement something we absolutely shouldnât roll ourselves (crypto, serious parsing, etc.). New services are fine if they have a clear API boundary and donât secretly depend on reaching into OpenClawâs internals. If it feels like âjust one more helper,â it probably belongs inside the existing modules instead.
Q: Do I need to write an ADR for every non-trivial change?
No. ADRs are for decisions that change how people think about the code: new modules, new cross-cutting patterns, deprecating old approaches. If youâre rearranging internal code but the mental model stays the same, a solid PR description is enough. When in doubt, ask in the #dev-architecture channel and weâll tell you if it deserves an ADR.
đ Published: