Skip to content

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

  • BaseRepository class with _scoped_query() that auto-filters by tenant_id on 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
  • DataStoreError hierarchy with store, operation, retry_safe fields 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