ADR-0028: Persistence Pattern Boundaries — Three Legitimate Strategies
- Status: Accepted
- Date: 2026-07-23
- Deciders: Architecture grilling session (issue #480)
Context
Issue #480 identified that three persistence patterns coexist in workers/src/: EntityRepository, EventStore, and direct D1. The initial concern was that this overlap was accidental and should be unified.
After research of industry literature (Martin Fowler, Greg Young, Cloudflare D1 docs, AWS Well-Architected Framework, PowerSync, RxDB), we concluded that all three patterns serve distinct legitimate purposes. The friction was not the existence of three patterns, but the lack of documented boundaries for choosing one over another.
Decision
Adopt the following persistence strategy boundaries, documented as the canonical decision tree for every new data access in the worker:
Decision Tree
Is the data concern an infrastructure/system table?
├── YES → Use Direct D1 (getDB().prepare())
│ Examples: auth queries, idempotency ledger, storage proxy
│
└── NO → Is the operation a READ or a WRITE?
├── READ → Use EntityRepository<T> via createReadHandler
│ Examples: list students, get class, find user
│
└── WRITE → Use createCommandHandler → applyEvent → events table
Examples: create/update/delete student, class, role, userBoundary Details
| Pattern | What it's for | Files | Test Adapter |
|---|---|---|---|
| Direct D1 | Infrastructure queries (auth, idempotency, system tables) | workers/src/db/queries.ts, route handlers for auth | mockD1.ts (SQL-string-matching) |
| EntityRepository<T> | Domain reads (list, findById, paginated queries with scope) | workers/src/storage/repository.ts, d1EntityRepo.ts, memoryEntityRepo.ts | MemoryEntityRepo (in-memory Map) |
| EventStore + applyEvent | Domain writes (create, update, delete) with atomic audit trail | workers/src/events/eventStore.ts, applyEvent.ts, createCommandHandler.ts | Mock EventStore implementations |
Rules
Infrastructure queries never go through EntityRepository or EventStore. Auth lookup, idempotency check, and system-table reads use direct D1. Performance-critical, no domain logic.
Domain reads never go through EventStore. The
eventstable stores audit history, not current state. Reads always query projection tables viaEntityRepository<T>.Domain writes always go through EventStore. Every create, update, or delete writes:
- A row to the
eventstable (audit trail) - A row to the relevant projection table Both in the same
D1EventStore.executeBatch()atomic operation. This is the command handler pattern from ADR-0023.
- A row to the
No entity mixes patterns in the same service file. A service file receives either
EntityRepository<T>(for reads) or usesapplyEvent(for writes), never both. If it needs both, split into two modules or accept the repository + a read-handler.Legacy entity-specific event tables (student_events, attendance_events, user_events) are DEPRECATED. All new writes go to the unified
eventstable. The legacy tables remain in the schema for backward compatibility with existing data but are not written to by new code. A future migration will backfill and drop them.
Consequences
Positive
- Clear decision framework. A developer adding a new endpoint can answer "Direct D1? Repository? EventStore?" with a single yes/no decision tree.
- Each pattern has a test strategy. Direct D1 → mockD1 (SQL strings), EntityRepository → MemoryEntityRepo, EventStore → mock EventStore.
- No more accidental mixing. The boundary lines are explicit and documented.
Negative
- Three patterns to learn. New developers must understand all three, but now they know when to use each instead of guessing.
- Migration burden. Legacy event tables need eventual cleanup. Low urgency.
Neutrals
- This ADR ratifies existing practice rather than prescribing new architecture. Most of the codebase already follows these boundaries — the ADR just names them.
- ADR-0023 remains the convergence plan. This ADR complements it by documenting why three patterns exist, not how to unify them.
References
- Issue #480 — Architecture: Unify three overlapping persistence patterns
- ADR-0021 — EntityRepository interface definition
- ADR-0023 — Server-Authoritative Model (command handler + event audit)
- ADR-0024 — Unified Error Handling
- Martin Fowler — Event Sourcing
- Greg Young — CQRS/ES litmus test
- AWS Well-Architected Framework — Serverless Lens
- PowerSync self-hosted architecture
- RxDB replication