ADR-001: Async SQLAlchemy ORM for Incident Persistence¶
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-05-23 |
| Deciders | Platform Engineering |
| Supersedes | — |
| Superseded by | — |
Context¶
The original persistence layer was a 6-line flat-file appender with no locking, no schema, and silent data corruption under concurrent writes. The API is async (FastAPI + uvicorn) and requires a persistence layer that does not block the event loop.
Decision¶
Adopt SQLAlchemy 2.0 async ORM (sqlalchemy.ext.asyncio) with asyncpg for
PostgreSQL in production and aiosqlite for local development and unit tests.
Rationale¶
- SQLAlchemy 2.0 async provides fully non-blocking ORM operations.
asyncpgis the highest-performance async Postgres driver available.aiosqliteenables zero-infrastructure unit test runs.- Enum types at the DB layer (
SAEnum) enforce domain constraints at rest, not only in application code. pool_pre_ping=Truehandles idle-connection drops from Postgres without manual reconnection logic.- Connection pooling via
pool_size+max_overflowprovides backpressure.
Alternatives Considered¶
- encode/databases: Simpler API but less mature; lacks ORM features needed for typed repository pattern.
- asyncpg raw: Higher performance ceiling but requires manual query construction and schema management — too much accidental complexity for this system's scale.
- Synchronous SQLAlchemy + thread pool: Works but introduces latency jitter under concurrency and complicates test fixtures.
Consequences¶
- All repository methods must be async.
- SQLite pool kwargs (
pool_size,max_overflow) must be conditionally excluded; handled in_build_engine()with anis_sqliteguard. - Schema changes require Alembic migrations (see ADR-003).