Provides architecture and engineering patterns for production async Python backends. Triggered when scaffolding, building, or reviewing Python backend features, FastAPI routes, SQLAlchemy models, or asyncio patterns. Enforces strict layer separation and domain-driven design.
Opinionated architecture for production async Python backends. Every layer has one job.
Read these when working in the relevant area:
references/layers.md — Full layer guide: services, repositories, controllers, factory, modelsreferences/fastapi.md — FastAPI route patterns, dependency injection, error mappingreferences/sqlalchemy.md — ORM models, repository pattern, async session handlingreferences/patterns-examples.md — Full code examples for all layersIMPORTANT: ask the user if they want you to start coding or explicitly invoke the planning-features skill to write a detailed plan.
HTTP Layer (FastAPI)
├── routes/ # Thin handlers. Validate input, call factory, return response.
├── dependencies.py # FastAPI Depends: session, config, request-scoped deps
└── error_handlers.py # AppError → HTTP response mapping. Only place HTTP status lives.
Domain Layer (no FastAPI imports)
├── models/ # Dataclasses. Input/output of every service and controller.
├── errors.py # AppError hierarchy. Domain errors only, no HTTP codes.
├── services/ # Protocol interface + implementation. One concern each. Never import each other.
├── repositories/ # SQLAlchemy ORM types stay here. Deserialise to domain models immediately.
├── controllers/ # Orchestrate services via TaskGroup. No business logic.
└── factory.py # Assemble everything. Singletons at startup, request-scoped with session.
Config
└── config.py # AppConfig dataclass. Reads from env/secrets at startup. Passed everywhere.
| Layer | Can do | Cannot do |
|---|---|---|
| Route handler | Parse request, call factory, return response | Business logic, direct DB access, service calls |
| Controller | Orchestrate services with TaskGroup | Business logic, DB access, HTTP concerns |
| Service | One domain concern, implement Protocol | Import other services, HTTP concerns, ORM types |
| Repository | SQLAlchemy queries, ORM↔model mapping | Business logic, calling services |
| Factory | Assemble singletons + request-scoped objects | Logic of any kind |
| Model | Pure data, dataclass or Pydantic | Methods with side effects |
slots=True always. frozen=True for immutable value objects, omit for mutable aggregates:
from dataclasses import dataclass
@dataclass(frozen=True, slots=True) # value object — immutable
class UserId:
value: str
@dataclass(frozen=True, slots=True) # output model — immutable
class Recipe:
id: str
title: str
cuisine: str
@dataclass(slots=True) # input — may be built incrementally
class CreateRecipeInput:
title: str
cuisine: str | None = None
from typing import Protocol, runtime_checkable
@runtime_checkable
class IRecipeService(Protocol):
async def get_by_user_id(self, user_id: str) -> list[Recipe]: ...
async def create(self, input: CreateRecipeInput) -> Recipe: ...
No ABC, no inheritance. Implementations just match the signature.
Bound logger per module, always:
import structlog