ADR-0018: Enforce typed contracts and Unit of Work owned persistence
- Status: Accepted
- Date: 2026-03-25
- Deciders: avm
- Supersedes:
- Superseded by:
Related ADRs
Context
Even with a good package layout, the backend quickly degrades if layers start talking through raw dict values, endpoints own transactions, and business flows bypass repositories or work directly with AsyncSession. That creates several systemic problems:
- boundaries between transport, application, and persistence become implicit;
- typed contracts are replaced with ad-hoc dictionaries that are harder to validate and evolve safely;
- services and workers start owning low-level transaction lifecycle without one shared policy;
- database reads and writes spread across endpoints, tasks, and helper modules;
- async runtime paths start mixing with sync-style procedural code.
Decision
The backend uses a typed, repository-driven, Unit-of-Work-owned execution model.
Boundary-contract rules:
- layers do not communicate through raw
dictvalues; - only
dataclassor Pydantic-based structures are allowed across layers; - transport payloads, application commands and results, repository inputs and outputs, and query/read models must be typed;
- raw
dictvalues are acceptable only at parsing or serialization edges or inside low-level infrastructure, not as cross-layer contracts.
Runtime-shape rules:
- endpoints stay
async-firstfunction handlers; - application services, repositories, query services, clients, and other non-endpoint components are designed
class-first; - endpoints own transport wiring and service invocation, but not transaction orchestration;
- endpoints receive already-assembled service or gateway dependencies through providers and do not build repositories, UoW objects, or low-level clients inside handlers;
- active backend paths must stay predictable and typed across the whole request.
Persistence rules:
- database access goes only through repositories or query services;
- the read path uses query services and typed immutable read models by default;
- endpoints, services, and workers do not talk to
AsyncSessionor SQL directly outside the repository boundary; - transactions are owned by the
Unit of Work; - the
Unit of Workis the single owner of commit, rollback, and transaction policy; - side effects that depend on a successful commit should respect the transaction boundary and ideally run post-commit.
Layer roles:
- endpoint: accept the request, validate the transport boundary, call the service, return a typed response;
- service: orchestrate the use case, use repositories and domain policies, and stay unaware of transport details;
- repository: the write boundary to the database on the write path;
- query service: a typed read boundary for read-heavy flows when repository abstraction is not enough;
- Unit of Work: own session lifecycle and transaction boundary.
Consequences
Positive
- cross-layer contracts become testable and safer to refactor;
- transaction ownership becomes centralized instead of leaking into endpoints or workers;
- persistence code stays concentrated in repositories and query services rather than spreading across runtime paths;
- the async API layer and class-based services get a clear role split.
Negative
- even simple scenarios require typed structures and repository or UoW wiring instead of shortcuts;
- the write path becomes stricter and does not allow fast direct-session hacks.
Neutral
- the specific UoW and repository internals may change as long as typed boundaries and ownership rules remain intact.
Alternatives considered
- allowing layers to exchange raw
dict[str, object]; - allowing direct
AsyncSessionaccess from endpoints and services; - building the backend in a procedural style with no class-based services or repositories;
- treating the repository pattern as optional and using it only selectively.
Follow-up work
- [ ] add static checks against raw
dictlayer contracts on the backend path - [ ] add a template scaffold for Unit of Work, repositories, and typed command or result contracts
- [ ] strengthen backend architecture validation against direct session access outside repository and UoW layers