Write production-grade Python backends. Not scripts. Not notebooks. Not prototypes. Mature, SOLID, testable systems that survive framework swaps, team growth, and 3am incidents.
Design Direction
Commit to an architectural stance before writing code:
- Purpose: What domain does this service own? What is its single bounded context?
- Boundaries: Where does the domain end and infrastructure begin? Draw the line.
- DI Strategy: How do dependencies flow? Constructor injection, framework DI, or container?
- Data Flow: Request -> Controller -> Service -> Repository -> Domain Entity -> Response DTO. Never skip layers.
CRITICAL: The architecture serves the domain, not the framework. If replacing Litestar with FastAPI would require rewriting business logic, the boundaries are wrong.
Architecture
Consult [architecture reference](references/architecture.md) for hexagonal patterns, layer rules, and directory structure.
Dependencies point inward. Domain imports nothing from infrastructure. Controllers are thin. Services orchestrate. Repositories hide persistence. The composition root wires everything together.
DO: Separate domain entities from ORM models with explicit mapping DO: Use Protocol or ABC for all ports -- repository interfaces, encryption, email, external APIs DO: Keep controllers under 150 lines -- they parse input, call a service, shape output DON'T: Import SQLAlchemy in your domain layer DON'T: Put business logic in route handlers -- that's a fat controller (AP-01) DON'T: Let framework types (Request, Response, AsyncSession) leak into services
SOLID Principles
Consult [SOLID reference](references/solid-principles.md) for Python-specific patterns and file size guidelines.
Every class has one reason to change. New behavior arrives via new code, not modified old code. Subtypes honor parent contracts. Interfaces stay small. High-level modules depend on abstractions.
DO: Split services by domain -- UserService, OrderService, not AppService DO: Use Protocol for structural typing at boundaries -- no inheritance required DO: Inject abstractions, never concretions DON'T: Create god modules >500 lines -- split by responsibility DON'T: Add raise NotImplementedError stubs -- that's ISP violation, split the interface DON'T: Pass concrete repository classes through your service constructors
Repository & Service Patterns
Consult [repository reference](references/repository-patterns.md) for Advanced Alchemy, transaction boundaries, and Unit of Work.
Repositories return domain entities, not ORM models. Repositories flush, not commit. Services own business logic. Controllers own HTTP concerns. Transaction boundaries live at the service call boundary.
DO: Map ORM model <-> domain entity in the repository (_to_entity, _to_model) DO: Use Advanced Alchemy's SQLAlchemyAsyncRepository when it fits -- it handles bulk ops, pagination, filtering DON'T: Write anemic services that just proxy repository calls -- add real logic or remove the layer DON'T: Call session.commit() inside repositories -- the controller or UoW commits
Dependency Injection
Consult [DI reference](references/dependency-injection.md) for Litestar Provide(), FastAPI Depends(), and anti-patterns.
Litestar: Provide() at app/router/controller/handler level. Layered, overridable, composable. FastAPI: Depends() with generator functions for session lifecycle. Cached per-request. Advanced Alchemy + Litestar: providers.create_service_dependencies() wires session, service, and filters automatically.
DO: Wire all dependencies in the composition root -- one place, one truth DON'T: Use Service Locator pattern (runtime container lookups) DON'T: Inject >5 dependencies into one class -- it violates SRP, split it
SQLAlchemy & ORM
Consult [SQLAlchemy reference](references/sqlalchemy.md) for 2.0 patterns, loading strategies, and session management.
Use mapped_column, not Column. Use select(), not session.query(). Set expire_on_commit=False always in async. Set lazy="noload" or lazy="raise" on all relationships -- opt in to loading per query.
DO: Use selectinload for one-to-many, joinedload for many-to-one DO: Use Annotated type aliases for reusable column definitions DON'T: Access relationships without explicit loading in async -- MissingGreenlet awaits DON'T: Write db.execute(select(...)) in controllers -- that's naked SQLAlchemy (AP-06)
Async Correctness
Consult [async reference](references/async-patterns.md) for blocking detection, TaskGroup, and cancellation safety.
Never block the event loop. Never share sessions across tasks. Never lazy-load in async context.
DO: Use httpx.AsyncClient, not requests DO: Use asyncio.to_thread() for CPU-bound or blocking-sync operations DO: Use asyncio.TaskGroup for concurrent independent operations DON'T: Call time.sleep() in async handlers -- use asyncio.sleep() DON'T: Fire-and-forget tasks without error handling -- exceptions vanish silently
API Design & Error Handling
Consult [API reference](references/api-design.md) and [error handling reference](references/error-handling.md) for schemas, DTOs, and exception patterns.
Separate request schemas from response schemas. Use machine-readable error codes. Domain exceptions map to HTTP at the boundary. Never expose internal details in error responses.
DO: Prefer msgspec.Struct for request/response schemas in Litestar -- native support, 5-12x faster than Pydantic DO: Use from_entity() factory methods on response schemas DO: Build a domain exception hierarchy (DomainError -> NotFoundError, ConflictError, etc.) DO: Register exception handlers at the app level for clean domain-to-HTTP mapping DON'T: Raise HTTPException in services -- that's framework coupling DON'T: Return dict[str, Any] from handlers -- use typed response models DON'T: Default to Pydantic in Litestar projects when msgspec does the job -- Pydantic is for FastAPI or when you need rich validators
Modern Python
Consult [modern Python reference](references/modern-python.md) for modern Python features, Protocol, deferred annotations, uv, and dataclass patterns.
Detect the project's Python version from pyproject.toml (requires-python), .python-version, or runtime. Latest stable Python (3.14+) brings deferred annotations, free-threaded builds, and TypeVar defaults. Use the latest features available at the project's version. str | None not Optional[str]. class Repo[T] not Generic[T]. StrEnum not string constants. match/case for complex dispatch. Use uv for all package management -- never pip install.
DO: Use Protocol for ports, dataclass(slots=True, frozen=True) for value objects, Pydantic at API boundaries only DO: Use datetime.now(timezone.utc) not datetime.utcnow() (deprecated since 3.12) DO: Use TypeVar defaults (3.13+) to simplify generic APIs DON'T: Use Pydantic for domain entities in hot paths -- several times slower than dataclasses DON'T: Use from __future__ import annotations on Python 3.14+ -- deferred annotations are native
Anti-Patterns
Consult [anti-patterns reference](references/anti-patterns.md) for the full AP-01 through AP-22 catalog.
The most dangerous patterns that appear in every Python backend. Know them, detect them, kill them:
- AP-01 Fat controller -- business logic in handlers
- AP-05 N+1 query -- lazy loads in loops
- AP-09 Blocking in async --
requests,time.sleep(), sync I/O on event loop - AP-11 Bare except --
except: passswallowsSystemExit - AP-14 Global mutable state -- module-level dicts mutated across requests
- AP-19 God class -- one class, thirty methods, eight responsibilities
The AI Slop Test
*Consult