Stable architecture
Goal: Removing or rewriting one layer does not collapse unrelated features. Dependencies point toward stability (domain rules), not toward frameworks.
Dependency rule (non-negotiable)
UI / HTTP / CLI → application services → domain ← adapters (DB, APIs)
- Domain must not import framework, ORM, or UI.
- Adapters implement ports defined by domain/application.
- Framework (Next.js routes, FastAPI handlers) is thin glue.
Violating this creates a house of cards: one ORM tweak breaks UI assumptions.
Layers (minimal)
| Layer |
Contains |
Must not contain |
| Domain |
Entities, invariants, pure rules |
SQL, HTTP, React |
| Application |
Use-cases, orchestration, transactions |
JSX, request objects leaking everywhere |
| Ports |
Interfaces/types for external systems |
Implementations |
| Adapters |
DB repos, HTTP clients, email senders |
Business rules |
| Delivery |
Routes, components, CLI |
Complex branching logic |
Skip layers you do not need — a script may be delivery + one function. Do not add folders "for architecture."
Houses of cards — detect and fix
| Symptom |
Likely cause |
Stabilizer |
| Changing A breaks B with no obvious link |
Hidden shared mutable state |
Pass data explicitly; narrow scope |
| Circular imports |
Domain ↔ infra coupling |
Extract port; move impl to adapter |
| "God" module (>500 lines, many reasons to change) |
Missing seams |
Split by use-case, not by technical type |
| Feature flags scattered in 20 files |
No single policy point |
One capability module |
| Tests need full app boot |
No ports |
Fake adapter behind interface |
| Types duplicated at every layer |
Leaky DTO mapping |
Map at boundary once |
Fail fast
- Validate at system boundaries (HTTP body, env vars, user input).
- Assert invariants inside domain (throw domain errors, not strings).
- Prefer compile-time guarantees (TypeScript strict, Rust types) over runtime checks deep in the stack.
- No silent defaults for security or money (
|| 'admin' is a card collapse waiting to happen).
Replaceability test
Before merging, ask:
- Can I swap the database adapter in one PR without touching domain?
- Can I test the use-case with an in-memory fake?
- If Next.js were removed, is there still a testable core?
If any answer is no for a non-trivial feature, add a seam before adding more features on top.
Vertical slices over horizontal layers
Prefer:
Feature A: route → use-case → adapter (complete)
Feature B: route → use-case → adapter (complete)
Over:
All routes → all services → all repos (half-wired, untestable middle)
Monolith vs services
Default: well-bounded monolith until proven scale/out-of-team needs split.
Split when: independent deploy cadence, different scaling profile, or hard failure isolation requirement — not because "microservices are modern."
Handoffs
- What to build first →
deliverable-first
- Proof while building →
real-time-testing
- UI composition →
ui-engineering
1---2name: stable-architecture3description: Design code that avoids houses of cards: clear module boundaries, dependency direction, fail-fast invariants, and replaceable adapters. Use when structuring a feature, splitting services, reviewing coupling, or when changes in one file break unrelated areas. Scope boundary: line-level clarity → `clean-minimal-code`; defining what done looks like → `deliverable-first`; running tests while editing → `real-time-testing`.4---5# Stable architecture67**Goal:** Removing or rewriting one layer does not collapse unrelated features. Dependencies point **toward stability** (domain rules), not toward frameworks.89## Dependency rule (non-negotiable)1011```12UI / HTTP / CLI → application services → domain ← adapters (DB, APIs)13```1415- **Domain** must not import framework, ORM, or UI.16- **Adapters** implement ports defined by domain/application.17- **Framework** (Next.js routes, FastAPI handlers) is thin glue.1819Violating this creates a house of cards: one ORM tweak breaks UI assumptions.2021## Layers (minimal)2223| Layer | Contains | Must not contain |24|---|---|---|25| **Domain** | Entities, invariants, pure rules | SQL, HTTP, React |26| **Application** | Use-cases, orchestration, transactions | JSX, request objects leaking everywhere |27| **Ports** | Interfaces/types for external systems | Implementations |28| **Adapters** | DB repos, HTTP clients, email senders | Business rules |29| **Delivery** | Routes, components, CLI | Complex branching logic |3031Skip layers you do not need — a script may be delivery + one function. Do not add folders "for architecture."3233## Houses of cards — detect and fix3435| Symptom | Likely cause | Stabilizer |36|---|---|---|37| Changing A breaks B with no obvious link | Hidden shared mutable state | Pass data explicitly; narrow scope |38| Circular imports | Domain ↔ infra coupling | Extract port; move impl to adapter |39| "God" module (>500 lines, many reasons to change) | Missing seams | Split by use-case, not by technical type |40| Feature flags scattered in 20 files | No single policy point | One capability module |41| Tests need full app boot | No ports | Fake adapter behind interface |42| Types duplicated at every layer | Leaky DTO mapping | Map at boundary once |4344## Fail fast4546- Validate at **system boundaries** (HTTP body, env vars, user input).47- Assert **invariants** inside domain (throw domain errors, not strings).48- Prefer **compile-time** guarantees (TypeScript strict, Rust types) over runtime checks deep in the stack.49- **No silent defaults** for security or money (`|| 'admin'` is a card collapse waiting to happen).5051## Replaceability test5253Before merging, ask:54551. Can I swap the database adapter in one PR without touching domain?562. Can I test the use-case with an in-memory fake?573. If Next.js were removed, is there still a testable core?5859If any answer is no for a non-trivial feature, add a seam before adding more features on top.6061## Vertical slices over horizontal layers6263Prefer:6465```66Feature A: route → use-case → adapter (complete)67Feature B: route → use-case → adapter (complete)68```6970Over:7172```73All routes → all services → all repos (half-wired, untestable middle)74```7576## Monolith vs services7778Default: **well-bounded monolith** until proven scale/out-of-team needs split.7980Split when: independent deploy cadence, different scaling profile, or hard failure isolation requirement — not because "microservices are modern."8182## Handoffs8384- What to build first → `deliverable-first`85- Proof while building → `real-time-testing`86- UI composition → `ui-engineering`