Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions template/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,17 @@ an ADR, run `make adr-check`.

## Async and Persistence

- The application is **async end to end**
([ADR 0017](docs/adr/0017-async-persistence-by-default.md)). Routes, handlers,
the message bus, repositories, query readers, and the unit of work are `async`;
persistence uses `create_async_engine` and `AsyncSession`. Keep **domain objects
synchronous** — business rules perform no I/O.
- **Async is the default — reach for it first.** The application is async end to
end ([ADR 0017](docs/adr/0017-async-persistence-by-default.md)): routes,
handlers, the message bus, repositories, query readers, and the unit of work
are `async`, and persistence uses `create_async_engine` and `AsyncSession`.
Keep **domain objects synchronous** — business rules perform no I/O.
- Never block the event loop inside `async def` (no synchronous DB driver, no
blocking call). `await` database work; offload genuinely blocking work to a
thread only with a documented reason.
blocking call). `await` database work. When a dependency is unavoidably
synchronous, wrap it with `asyncify()` from
[Asyncer](https://asyncer.tiangolo.com/) (by FastAPI's author) —
`await asyncify(blocking_fn)(arg)` runs it in a worker thread
(`uv add asyncer`) — rather than leaving the blocking call inline.
- The default database is **PostgreSQL** via `asyncpg`; **SQLite** via `aiosqlite`
is the optional alternative
([ADR 0018](docs/adr/0018-postgresql-default-with-pgvector.md)). Select the
Expand Down
13 changes: 10 additions & 3 deletions template/docs/adr/0017-async-persistence-by-default.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,15 @@ semantics. The domain layer is unaffected and remains trivially unit-testable.

## Agent Guidance

- Make every adapter, service-layer, and entrypoint I/O path `async`/`await`.
- Never call blocking I/O inside an `async def`; offload to a thread only with a
documented reason.
- Reach for async first: a new adapter, handler, route, or client is `async` by
default. Make every adapter, service-layer, and entrypoint I/O path
`async`/`await`.
- Never call blocking I/O inside an `async def`. When a dependency is
unavoidably synchronous, wrap the call with `asyncify()` from
[Asyncer](https://asyncer.tiangolo.com/) (by FastAPI's author) — e.g.
`await asyncify(blocking_fn)(arg)` — which runs it in a worker thread instead
of blocking the event loop (`uv add asyncer`). Do not paper over a blocking
call by leaving it inline.
- Keep domain methods synchronous and free of I/O.
- Use `AsyncSession` and `await` commits/queries; do not mix a sync `Session`
into the request path.
Expand All @@ -61,5 +67,6 @@ semantics. The domain layer is unaffected and remains trivially unit-testable.

- [SQLAlchemy asyncio extension](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html)
- [FastAPI: async and await](https://fastapi.tiangolo.com/async/)
- [Asyncer: `asyncify`](https://asyncer.tiangolo.com/tutorial/asyncify/)
- [pytest-asyncio](https://pytest-asyncio.readthedocs.io/)
- [ADR 0006: Async Is an Explicit End-to-End Choice](0006-async-is-an-explicit-end-to-end-choice.md)
Loading