ADR-0016: DAO / Repository Pattern for Data Access¶
Status: Accepted — Not Yet Implemented Date: 2026-04-10 Session: 35
Context¶
A data persistence audit found that Curaway has no centralized data
access layer. 50 service files write raw SQLAlchemy queries inline
across 6 data stores (PostgreSQL, Neo4j, Qdrant, Redis, R2, QStash).
Each file has its own try/except error handling. Tenant isolation
is opt-in — every query must manually add .where(tenant_id == ...).
A single missed filter is a cross-tenant data leak.
The upcoming case record porting feature touches 4 stores in one flow. The LLM fallback gateway needs centralized retry. Multilingual and multicurrency support need consistent data contracts. And the microservices readiness audit (doc #20) identified shared DB sessions and scattered data access as two of three critical extraction blockers.
Decision¶
Introduce a thin repository layer for PostgreSQL (Phase 1), then wrap Neo4j, Redis, R2, and QStash in domain-specific DAOs (Phase 2-3).
What we're doing¶
BaseRepositoryclass with_scoped_query()that auto-filters bytenant_idon every read — tenant isolation enforced at the data layer, not scattered across 50 service files- 5 typed repositories (Patient, Case, FHIR, Document, Provider) with domain-specific methods — not generic CRUD
DataStoreErrorhierarchy withstore,operation,retry_safefields for circuit breaker integration- Each repository owns its transaction boundary — prerequisite for microservices extraction
What we're NOT doing¶
- Generic
Repository[T]with abstract CRUD — too much abstraction for a seed-stage product - Unit of Work pattern — adds complexity without clear benefit at this scale
- Re-wrapping Qdrant — already well-abstracted via
semantic_search_service.py - Repository-level caching — cache stays in
cache_service.py
Consequences¶
Positive:
- Tenant isolation enforced by default, not by developer memory
- Unified error taxonomy enables circuit breakers and retry policies
- Per-domain transaction ownership unblocks microservices extraction
- New data access patterns get one place to add audit logging
- Found real bug: _generate_case_number() has no tenant filter
Negative: - Every service migration is a small refactor (replace inline queries with repository calls) — ~2 hours per service - Developers must learn the repository pattern (thin — 5 methods per repository, not a heavy ORM) - Slight indirection: service → repository → SQLAlchemy instead of service → SQLAlchemy directly
Neutral: - No performance impact — repositories are pass-through wrappers, same queries underneath
Implementation¶
- Full spec:
docs/specs/ai-steer/dao-layer-steer.md+docs/specs/dao-layer-feature.md - Phase 1: PostgreSQL repositories (~7 hours)
- Phase 2: GraphDAO + CacheDAO (~1 week)
- Phase 3: StorageDAO + QueueDAO (~1 week)
- Opus/Sonnet tier tags on implementation checklist
References¶
- Data persistence audit (Session 35)
- Microservices readiness:
docs/architecture/20-microservices-readiness.md - Subagent enforcement: architecture-reviewer, code-reviewer, compliance-reviewer, platform-integrity-checker