Architecture — Universal Principles
Stack-agnostic. Applies to any language, any framework.
Architecture — Universal Principles
Stack-agnostic. Applies to any language, any framework.
TL;DR (human)
Six rules. They scale from a one-package project to a 30+ package monorepo. They are the price of admission for letting agents touch your code without supervision.
- Name every boundary. If you cannot tell an agent "this code goes in package X", the boundary does not exist yet.
- One contract package owns schemas + error model. Everything else depends on it; it depends on nothing internal.
- Every change to that contract goes through a written decision (ADR / RFC). The document is the artifact.
- No
any/dyn/interface{}at any external boundary. Parse with a runtime schema. - Errors are typed with stable codes. The client (or another agent) can pattern-match without reading strings.
- Files have size budgets enforced by a gate. Reviewability is a feature.
For agents
Rule 1 — Name every boundary
Every directory that an agent might write code into must answer: "what is this for, and what is it not for?" in one sentence.
- Maintain a top-level routing table mapping intent ("I want to change X") to location ("edit package Y"). Template:
../../templates/AGENTS.md.template.md. - If two packages could plausibly own the same change, the boundary is wrong. Fix the boundary or merge the packages.
- Group packages into 4–7 logical groups (e.g. "contracts + foundation", "runtime + flow", "security + collaboration"). Agents triage faster by group than by alphabetical name.
Failure mode prevented: agents inventing new packages or piling code into the largest existing file because no rule said where it goes.
Rule 2 — One contract package owns schemas + error model
Pick one package (call it core or contracts). It contains:
- All shared types / schemas (Zod, Pydantic, Protobuf — your choice).
- The error class hierarchy + the central error-code constants.
- Nothing else. No business logic. No I/O.
Constraints:
- This package has no internal dependencies. It can depend on
zodandstd, nothing else. - It has a hard size budget (e.g. 25 KB gzipped). The budget is a CI gate. Hitting the budget forces a real conversation about what belongs in the contract layer.
Failure mode prevented: circular dependencies, schema drift between packages, agents copy-pasting "the same" schema with a one-field difference.
Rule 3 — Decisions are written down before they ship
Two artifacts:
- ADR (Architecture Decision Record) — for choices that affect the codebase's shape. Numbered. Append-only. Status: Proposed / Accepted / Superseded / Tombstoned. Template:
../../templates/ADR.template.md. - RFC (Request for Comment) — for choices that affect external contracts (public API, wire format, plugin protocol). Has a review window. Promotes to ADR when accepted. Template:
../../templates/RFC.template.md.
Rules:
- An agent proposing a structural change without an ADR is proposing tech debt. Reject the PR; ask for the ADR first.
- An agent proposing a breaking change without an RFC is proposing an unannounced break. Reject the PR; ask for the RFC.
- ADRs/RFCs are the change. The code is the implementation of the change.
Failure mode prevented: "we decided" without anyone able to find the decision; future agents reverting it because they cannot see why it was made.
Rule 4 — Parse, don't validate, at every external boundary
External boundary = anywhere bytes enter the process from outside the trust boundary: HTTP, IPC, JSON-RPC, file I/O, env vars, CLI args, message-bus payloads.
- Define a schema. Parse the input. Use the parsed type. If parsing fails, raise a typed error with a stable code.
- Do not type-cast unparsed input. Do not "trust the API contract on the other side". Parse.
- The parsed type is the only type that flows into the rest of the system. Untyped data is sandboxed at the edge.
Failure mode prevented: runtime errors deep in the system caused by an upstream caller's drift; agents writing handler code that assumes the wrong shape and breaks silently.
Rule 5 — Typed errors with stable codes
Define one base error class. Every other error in the system subclasses it. Each error has:
- a stable string code (
NAMESPACE_REASON, all-caps, snake-case), - a human message (intl-keyed in user-facing surfaces),
- an optional
hint(one-line suggestion to the caller), - an optional
docsUrl(link to the error doc).
Constraints:
- Never
throw new Error('...')at a boundary. Wrap in a typed subclass. - The dispatcher / HTTP layer serializes typed errors with
code+message+hint+docsUrl. Unknown thrown errors become an opaqueINTERNAL_ERROR— never leak stack traces or raw strings. - Codes are append-only. Renames go through an ADR.
Failure mode prevented: clients pattern-matching on error message strings (which drift); agents inventing new error shapes per package; opaque failures in production.
Rule 6 — File-size budgets
Pick budgets per file kind. Enforce them in a gate.
Example budgets (calibrated for TS/React; adjust per language):
- View / component files: 300 lines.
- Logic / module files: 500 lines.
- Test files: 800 lines (often unavoidable for table-driven tests).
Rules:
- The gate is baseline-shrink-only: an existing file over budget is grandfathered, but new code in that file must shrink it; new files must respect the budget.
- Hitting the budget = extract. Do not lower the budget to fit. Do not split into
\<file\>-2.\<ext\>.
Failure mode prevented: files becoming unreviewable; agents losing context inside a 1500-line component; reviewers approving PRs they cannot read.
See also
adr-pattern.md— how to write an ADR.rfc-pattern.md— when an ADR is not enough.error-hierarchy.md— the error model in detail.file-size-budget.md— budget calibration + gate impl.../governance/README.md— merge rules that protect these boundaries.
Architecture — TS / Node ≥22 / pnpm Monorepo (Concrete)
Copy-paste-ready recipes that implement [`universal.md`](./universal.md) on a TypeScript stack. Calibrated on a real multi-package, multi-app monorepo built primarily by AI agents over ~1 year.
AI / LLM Safety Pattern
How to ship LLM-powered features without prompt injection, jailbreaks, data leakage, hallucinated tools, or runaway costs.