Architecture and engineering patterns for production Python backend projects. Use this skill whenever the user asks to scaffold, build, or review any Python backend feature — including services, controllers, repositories, factories, models, FastAPI routes, or config. Also trigger when the user asks about SQLAlchemy, asyncio patterns, Protocol interfaces, structlog, AppConfig, error hierarchies, or where to put business logic. Enforces strict layer separation; domain never touches HTTP, repositories own ORM types, services never import each other, controllers orchestrate via TaskGroup, factory assembles everything.
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 handlingIMPORTANT: ask the user if they want you to start coding or explicitly invoke the feature-blueprint 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