111 lines
3.2 KiB
Markdown
111 lines
3.2 KiB
Markdown
# Async Transaction Boundaries
|
|
|
|
!!! info "Primary sources"
|
|
- [SQLAlchemy transactions](https://docs.sqlalchemy.org/en/21/orm/session_transaction.html)
|
|
- [SQLAlchemy asyncio extension](https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html)
|
|
- [SQLAlchemy connections](https://docs.sqlalchemy.org/en/21/core/connections.html)
|
|
|
|
??? abstract "Decision metadata"
|
|
- Status: adopted
|
|
- Decision level: mandatory
|
|
- Applies to: api-runtime, workers, tests
|
|
- Last reviewed: 2026-06-17
|
|
|
|
---
|
|
|
|
## Purpose
|
|
|
|
Define consistent transaction demarcation for async SQLAlchemy so write behavior is predictable, rollback semantics are clear, and concurrent request flows remain safe.
|
|
|
|
---
|
|
|
|
## Scope and Non-Goals
|
|
|
|
- In scope: transaction ownership, write/read policy, exception and rollback behavior, nested transaction guidance.
|
|
- Out of scope: business-domain validation rules and cross-service distributed transactions.
|
|
|
|
---
|
|
|
|
## Rules
|
|
|
|
- Every mutating use case must run inside an explicit transaction boundary.
|
|
- Prefer `async with session.begin():` for write units.
|
|
- Keep transaction ownership at service/use-case boundary, not deep in helper internals.
|
|
- Read paths should not auto-upgrade into hidden write behavior.
|
|
- On exception in a transaction block, rely on rollback semantics and propagate or map exceptions intentionally.
|
|
|
|
---
|
|
|
|
## Recommended Patterns
|
|
|
|
### Pattern A: Single write unit
|
|
|
|
```python
|
|
async def create_order(session: AsyncSession, payload: OrderIn) -> Order:
|
|
async with session.begin():
|
|
order = Order(...)
|
|
session.add(order)
|
|
# additional writes...
|
|
return order
|
|
```
|
|
|
|
### Pattern B: Explicit read flow
|
|
|
|
```python
|
|
async def get_order(session: AsyncSession, order_id: UUID) -> Order | None:
|
|
stmt = select(Order).where(Order.id == order_id)
|
|
return await session.scalar(stmt)
|
|
```
|
|
|
|
### Pattern C: Nested transaction (only when required)
|
|
|
|
```python
|
|
async with session.begin():
|
|
# outer transaction
|
|
async with session.begin_nested():
|
|
# savepoint-scoped operation
|
|
...
|
|
```
|
|
|
|
Use nested transactions only when partial failure semantics are explicitly required.
|
|
|
|
---
|
|
|
|
## Exception and Rollback Policy
|
|
|
|
- Write block fails: transaction context rolls back.
|
|
- Caller decides whether to translate exception (for example to domain/API errors).
|
|
- Do not swallow DB exceptions silently; map or re-raise intentionally.
|
|
|
|
---
|
|
|
|
## Anti-Patterns
|
|
|
|
- Multiple commits scattered across one logical use case.
|
|
- Helper functions that commit/rollback without caller awareness.
|
|
- Mixing implicit and explicit transaction styles in confusing ways.
|
|
- Using savepoints as a default pattern rather than a targeted tool.
|
|
|
|
---
|
|
|
|
## Operational Checks
|
|
|
|
- All mutating service functions declare one clear transaction boundary.
|
|
- No repository/helper performs hidden commit calls.
|
|
- Transaction style is consistent across handlers and workers.
|
|
|
|
---
|
|
|
|
## Testing Checks
|
|
|
|
- Success path test verifies expected durable writes.
|
|
- Failure path test verifies rollback behavior.
|
|
- Tests cover concurrency-sensitive write flows.
|
|
- Savepoint usage (if present) has dedicated behavior tests.
|
|
|
|
---
|
|
|
|
## Migration Notes
|
|
|
|
- First stabilize session scope, then normalize transaction ownership.
|
|
- Replace ad hoc commit patterns incrementally with bounded write units. |