Skip to content

Repository files navigation

s1t-python-backend-templates

An architecture you can read. Two services, zero shared code, strict DDD in every context -- a production-shaped monorepo template where the patterns are the product and the features exist to prove them.

CI Python 3.12+ Litestar 2.24+ License: MIT

-> The landing page tells this story visually.


Topology

┌──────────────────────┐   video_uploaded (stream)   ┌──────────────────────┐
│   litestar_backend   │ ────────────────────────--> │  event_microservice  │
│  HTTP · Postgres     │                             │  FastStream · SAQ    │
│  outbox · auth · SSE │ <--──────────────────────── │  Valkey join · jobs  │
└──────────────────────┘    video_status (stream)    └──────────────────────┘

Only wire contracts cross the boundary -- not a single shared import. Each service is a standalone uv project (own pyproject.toml, lock, Dockerfile) you could extract to its own repo unchanged. Contracts: docs/contract/.

Anatomy of a context

Every bounded context keeps the same four layers; imports point inward only -- context boundaries and layer direction are enforced by import-linter in CI, pre-commit, and the Docker test gate:

adapters --> ports --> app --> domain
controllers  facades   use cases  pure stdlib
engines      repos     Protocols  aggregates, VOs, events

Plus provider.py (Dishka DI -- the only wiring point) and config.py (own env prefix). Siblings talk through ACLs; every decision has an ADR. Full rules: docs/architecture.md.

Pattern catalog

Pattern What it buys Where
Transactional outbox row + event commit atomically; relay drains to a Valkey Stream shared/adapters/driven/outbox_relay.py · ADR 0031
Inbox dedup at-least-once delivery, exactly-once effect via event_id inbox delivery-guarantees.md
Idempotent write Idempotency-Key claimed in the write's own transaction; a retry replays the first response shared/adapters/driven/postgres/idempotency.py · ADR 0033
Composite auth chain JWT -> API-key -> static token, first match wins, fail-closed auth/ports/driven/composite_token_resolver.py · ADR 0032
Keyset pagination opaque cursors, stable pages under writes, one generic Page[T] shared/generics/pagination.py
Integration-event envelope event_id + version + occurred_at on every wire event by construction shared/generics/integration_event.py
Graceful drain lifespan managers own their background tasks and stop them inside the grace window media_example/adapters/lifespan_manager.py

One request, every pattern

# The example endpoints are deliberately unauthenticated -- guard before deploying.
curl -X POST http://localhost:8000/videos \
  -H "Content-Type: application/json" \
  -d '{"source_key": "uploads/demo.mp4"}'
# -> 202 {"id": "<uuid>", "status": "PENDING", ...}

The API writes the video row + outbox message in one transaction; a relay publishes video_uploaded to a Valkey Stream; the consumer enqueues three SAQ jobs (stt, plagiarism, transcode); the worker joins their completion in Valkey and publishes video_status back; the backend drives the video through PENDING -> PROCESSING -> DONE/FAILED and broadcasts each transition to the SSE feed at /videos/feed. Watch it in the SAQ panel and the admin log viewer.


First run

cp .env.example .env      # minimal; every knob: .env.full.example
openssl rand -hex 32      # paste into AUTH_ADMIN_TOKEN=...
docker compose up --build # Postgres, Valkey, API, consumer, SAQ worker

Dev is container-only: docker-compose.override.yml is auto-merged and bind-mounts src/ over the image copy, so code changes need no rebuild. Production ignores it: docker compose -f docker-compose.yml up.

What URL
API http://localhost:8000
Admin login -> dashboard + log viewer http://localhost:8000/admin/login
OpenAPI UI http://localhost:8000/schema/swagger -- needs one line: copy SECURITY_CSP (dev variant) from .env.full.example into .env
Backend Prometheus metrics http://localhost:8000/metrics
SAQ admin panel (jobs, retry/abort) http://localhost:8081
Consumer / worker metrics http://localhost:9101/metrics, http://localhost:9102/metrics

Admin auth: paste AUTH_ADMIN_TOKEN at /admin/login (HttpOnly cookie, SameSite=Strict). Empty token disables auth with a startup warning -- dev only; APP_ENV=prod rejects it at boot. The SAQ panel takes SAQ_WEB_PASSWORD for HTTP Basic (user admin); details: saq.md.

Project layout

src/
├── litestar_backend/         Litestar API (own pyproject.toml + uv.lock + Dockerfile)
│   ├── src/
│   │   ├── shared/           Cross-cutting kernel: config, errors, Postgres/Valkey/metrics infra
│   │   ├── root/             Entrypoints + Dishka container assembly
│   │   ├── auth/             Users (argon2id) + JWT + API keys + admin token, role guards
│   │   ├── admin/            Admin dashboard; admin/log/ = file-tail log viewer (SSE, export)
│   │   ├── media_example/    GOLDEN CONTEXT: outbox + relay + SSE, full S-DDD layering
│   │   └── db_example_litestar/  Hybrid CRUD example: advanced-alchemy + SQLAlchemyDTO
│   ├── migrations/           yoyo migrations, one folder per context
│   ├── static/               Jinja templates + assets, mirrors the context tree
│   └── tests/                <context>/{unit,flow,integration,e2e} -- mirrors src/
└── event_microservice/       FastStream consumer + SAQ worker (own pyproject + lock)
    ├── src/
    │   ├── shared/           Own Valkey client, logging, base errors
    │   ├── root/             Container + two entrypoints: consumer, saq_worker
    │   └── media_processing/ Bounded context: jobs, join policy, SAQ queue port
    └── tests/

Picking an example to copy: start from media_example for anything with real business logic (full layering, domain events, outbox, tests at all four levels). Use db_example_litestar only for thin CRUD with no invariants.

Technology map

Technology Where it lives Use it for
Litestar 2.24+ litestar_backend adapters HTTP controllers, SSE, guards, exception handlers
Dishka provider.py per context, root/composition All wiring; business code never builds its dependencies
Pydantic / pydantic-settings ports/driving schemas, config.py HTTP boundary validation and env config -- nowhere else
msgspec outbox payloads, wire events Dataclass-shaped wire payloads (faster than json/Pydantic)
SQLAlchemy 2.0 (plain) media_example The default DB pattern to copy: explicit session, mappers, outbox
advanced-alchemy db_example_litestar only Thin CRUD where a repository/service + SQLAlchemyDTO suffice
yoyo-migrations migrations/<context>/ Schema changes, applied in the context's lifespan
Valkey streams, litestar.channels, join store Event transport between services; SSE fan-out; job-join state
FastStream event_microservice consumer Reacting to stream events (the "HTTP of the event world")
SAQ event_microservice worker Heavy/retryable jobs: CPU via process pool, blocking I/O via threads
structlog shared/logging.py in both services Structured JSON logs; stdout + the JSONL file the admin UI tails
prometheus_client shared (backend), worker adapters Counters/gauges; multiprocess mode makes APP_WORKERS a free knob
Jinja static/ + admin controllers Server-rendered admin pages; no SPA build step

Tests

Canonical path is Docker Compose -- same toolchain as the app images:

docker compose run --build --rm litestar_backend_test                        # ruff + mypy + pytest
docker compose run --build --rm litestar_backend_test pytest -m unit -q      # any subset
docker compose run --build --rm event_microservice_test

Local uv inner loop (from the service root, e.g. src/litestar_backend/):

uv run pytest -m "unit or flow"   # instant, no DB
uv run pytest                     # full suite -- needs Docker (testcontainers)
uv run ruff check . && uv run mypy && uv run lint-imports

Tests are grouped by context first, then level -- tests/<context>/{unit,flow, integration,e2e}/ mirrors src/. Pick a level with -m <level> (a marker set from the path): unit (domain, no mocks), flow (use cases, mocked interfaces), integration (real Postgres/Valkey), e2e (full app). The Taskfile wraps these (task test, task be:unit) and pre-commit runs ruff/mypy/gitleaks on every commit -- see docs/development.md.

Documentation

Section Contents
docs/architecture.md Both services: contexts, layers, error hierarchy, DI, lifespan, invariants.
docs/contract/ Wire contracts: shared rules (common.md), video_uploaded, video_status, user_registered.
docs/adr/ Project-scope ADRs (MADR); service- and context-scope trees live with their service.
src/litestar_backend/ Backend run card, then docs/: contexts/, subsystems/, infra/, service ADRs.
src/event_microservice/ Worker run card, then docs/: contexts/media_processing.md, infra/ (faststream, saq), service ADRs.
docs/development.md Dev workflow: Taskfile, pre-commit gate, pinned toolchain.
docs/infra/ Platform substrate: Postgres, Valkey.

A template, not a framework: fork it, rename it, delete what you don't need.

MIT

About

Production-shaped Litestar monorepo template: strict DDD, transactional outbox, event-driven two-service topology

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages