Files
prompts/docs/skills/fastapi-async-sqlalchemy-modernization/references/session.md
T
John Lancaster 3347443ca9 formatting
2026-06-19 01:29:05 -05:00

4.0 KiB

Async SQLAlchemy Session Management

!!! info "Primary sources" - SQLAlchemy asyncio extension - SQLAlchemy session basics - FastAPI dependencies with yield

??? abstract "Decision metadata" - Status: adopted - Decision level: mandatory - Applies to: api-runtime, workers, tests - Last reviewed: 2026-06-17


Purpose

Define one canonical session model for FastAPI + SQLAlchemy asyncio:

  • configure one shared session factory,
  • create one AsyncSession per request or per unit-of-work,
  • never share one AsyncSession across concurrent tasks.

Scope and Non-Goals

  • In scope: session factory creation, FastAPI dependency wiring, request/task scoping, transaction demarcation.
  • Out of scope: ORM model design, query optimization strategy, schema migration tooling.

Rules

  • Create async_sessionmaker once from app-owned AsyncEngine.
  • Use a fresh AsyncSession for each request or explicit unit-of-work.
  • Do not share AsyncSession across asyncio.gather() or parallel tasks.
  • Prefer direct dependency injection over global scoped-session patterns in new code.
  • Use explicit transaction boundaries (async with session.begin():) for writes.

Canonical FastAPI Dependency Pattern

from collections.abc import AsyncIterator

from fastapi import Depends, Request
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker


def get_session_factory(request: Request) -> async_sessionmaker[AsyncSession]:
    return request.app.state.session_factory


async def get_db_session(
    session_factory: async_sessionmaker[AsyncSession] = Depends(get_session_factory),
) -> AsyncIterator[AsyncSession]:
    async with session_factory() as session:
    yield session

Route usage:

from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession

router = APIRouter()


@router.post("/items")
async def create_item(session: AsyncSession = Depends(get_db_session)) -> dict:
    async with session.begin():
    # write operations here
    ...
    return {"status": "ok"}

Configuration Guidance

Typical session factory setup:

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

session_factory = async_sessionmaker(
    engine,
    class_=AsyncSession,
    expire_on_commit=False,
)

Notes:

  • expire_on_commit=False is commonly preferred in asyncio applications to reduce accidental post-commit reload behavior.
  • AsyncSession.refresh() is preferred over broad expiration patterns when state refresh is needed.

Concurrency Rules

  • One session per concurrent task.
  • If work fans out into parallel tasks, each task receives its own AsyncSession.
  • Pass sessions explicitly to service functions; avoid mutable global session state.

Anti-Patterns

  • A singleton/global AsyncSession reused across requests.
  • Sharing one AsyncSession across parallel tasks.
  • Hidden session creation in lower repository helpers with no caller control.
  • Mixing commit/rollback ownership across layers without a declared boundary.

Operational Checks

  • Exactly one async_sessionmaker is registered in app lifecycle.
  • Request handlers receive sessions from one canonical dependency.
  • No code path creates AsyncSession in module import side effects.
  • Background jobs and API handlers each create task-local sessions.

Testing Checks

  • Dependency override exists for test session factory.
  • Rollback behavior is verified for failed write units.
  • Parallel-task tests verify no shared AsyncSession instances.
  • Lifespan tests confirm session factory is initialized and teardown-safe.

Migration Notes

  • If current code uses global/shared sessions, fix scope first before refactoring query style.
  • If legacy sync patterns are present, keep session boundary rules stable while migrating incrementally.