FastAPI backend
The second backend, and the one that proves the point. It implements the same contract as the Go reference and passes the same 16 contract tests, with not a single change to the tests or the frontend. Different language, different framework, different async model, identical behaviour.
It follows the same hexagonal shape: thin route handlers, a framework-free use-case layer, repository ports, injected adapters. If you have read the Go backend, this will feel familiar on purpose. That familiarity is the deliverable.
Layout
app/domain entities, enums, errors. Imports nothing from FastAPI or asyncpg.
app/usecase business rules + Protocol ports. reservation.py is the star.
app/adapter
postgres.py repositories over asyncpg
redis_adp.py distributed lock, rate limiter, refresh store
broker.py RabbitMQ via aio-pika
paymentgw.py httpx client for the fake gateway
memory.py in-process fakes for unit tests
app/platform clock, uuid, bcrypt, JWT token service
app/transport/http DTOs, error envelope + handlers
app/main.py composition root: app factory, lifespan wiring, sweeper, worker
app/config.py env-driven configuration
Run it
From the repo root, select this backend and bring the stack up:
# set COMPOSE_PROFILES=fastapi in .env, then:
make up
# or, one-off without editing .env:
COMPOSE_PROFILES=fastapi docker compose up -d --build
The gateway routes /api to whichever backend holds the backend network alias, so
the API is at https://localhost/api regardless of which backend is active.
Test it
Unit tests, no infrastructure required, including the overselling-under-concurrency proof (500 concurrent buyers, 100 tickets, exactly 100 succeed):
cd backends/fastapi
python -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
.venv/bin/python -m pytest
The shared contract suite, against the running backend:
cd contract/tests
TARGET_URL=https://localhost/api pytest -v
Notes that bit us, so they will not bite you
- asyncpg dislikes
sslmode. It is a libpq parameter, not an asyncpg one, so the composeDATABASE_URLfor this backend omits it. hmac.compare_digestrejects non-ASCII strings. The webhook handler catches that and treats a junk signature as invalid (401) rather than crashing to 500. The contract fuzzer found this; see the commit history.- FastAPI's default validation error is a 422 with its own body shape. We override it to the contract's shared 400 envelope so malformed requests stay conformant.