A working starter kit, a reference implementation and a practical guide to Clean Architecture in modern Python - in one repository.
FastAPI · SQLAlchemy 2 async · Alembic · uv · Ruff · mypy strict · pytest · executable architecture tests · Docker · GitHub Actions · semantic-release · Docusaurus docs
📖 Documentation · Architecture · Decisions (ADR) · Replace the example domain
make proofARCHITECTURE PROOF
Dependency rule PASS ADR-001, ADR-006
Feature isolation PASS ADR-001
Repository contract PASS ADR-008
Optimistic concurrency PASS ADR-010
Transaction boundary PASS ADR-004
Events after commit PASS ADR-005
Authorization in the application ring PASS ADR-009
HTTP error contract PASS no ADR
Example removal PASS ADR-001
9/9 executable guarantees passed
Every "clean architecture" repository has the folders and the diagram. This one has a
short list of specific, falsifiable guarantees, each backed by real tests you can read,
each traced to the decision it implements, and a command that runs exactly those tests and
fails the build when one stops being true. The list lives in proofs.toml;
make proof cannot be green by default: a guarantee with no evidence is reported as such.
| Guarantee | What is checked | Where |
|---|---|---|
proof:dependency-rule |
every module imports inward; domain/application import nothing third-party; falsified on 13 synthetic violations |
tests/architecture |
proof:feature-isolation |
no feature imports another; shared imports no feature |
tests/architecture |
proof:repository-contract |
in-memory and SQL repositories pass the same contract | tests/integration |
proof:optimistic-concurrency |
two readers race on one row: the stale write raises ConflictError, HTTP says 409 |
tests/integration, tests/api |
proof:transaction-boundary |
commit before the response is sent; failed commit → 500, nothing persisted | tests/api |
proof:events-after-commit |
handlers run after commit and see committed rows; rollback dispatches nothing | tests/api, tests/cli |
proof:authorization-in-application |
who may act is decided on an Actor in use cases, testable without HTTP |
tests/tournaments, tests/api |
proof:error-contract |
one error envelope for 404/405/500, request id on every response | tests/api |
proof:example-removal |
init_project.py --remove-example leaves a project that lints, tests, migrates and serves |
tests/template |
What this is not. It is not a proof that the architecture is correct, complete or
suited to your problem, and it does not cover what your own code will do. It is the list of
properties this template promises, kept honest by CI. Decisions without a runtime property
(no DI library, no mediator, the stack) say so in their ADR: No executable proof.
git clone https://github.com/davidcohenDC/python-clean-architecture-template
cd python-clean-architecture-template
make install # uv sync + git hooks
make run # migrations + uvicorn with reload → http://localhost:8000/docsNo database? make run-memory. PostgreSQL? docker compose up. Everything CI runs: make check.
Without make
uv sync --all-extras
git config core.hooksPath .githooks
uv run alembic upgrade head
uv run uvicorn cleanarch.main:app --reloadflowchart TB
subgraph B["bootstrap · composition root: knows every concrete class"]
subgraph A["adapters · infrastructure (SQLAlchemy, in-memory, event bus) | http (FastAPI)"]
subgraph AP["application · use cases + ports (Protocols)"]
D["domain · entities, value objects, events, rules · stdlib only"]
end
end
end
style D fill:#16a34a,color:#fff,stroke:none
style AP fill:#2563eb,color:#fff,stroke:none
style A fill:#d97706,color:#fff,stroke:none
style B fill:#6b7280,color:#fff,stroke:none
Dependencies point inward, always. infrastructure, http and cli sit on the same ring
and never import each other; they meet only in bootstrap. The example feature is driven by
both an HTTP router and a command line (python -m cleanarch tournaments ...) that call the
same use cases through the same ports.
sequenceDiagram
autonumber
participant C as Client
participant R as http/router.py
participant U as application/use_cases.py
participant T as domain/tournament.py
participant P as infrastructure/sqlalchemy
participant E as InProcessEventBus
C->>R: POST /tournaments/{id}/start
R->>U: StartTournament.execute(id) (built by Depends, ports overridden in bootstrap)
U->>P: repository.get(id)
P-->>U: Tournament
U->>T: tournament.start()
T-->>U: DomainResult(new Tournament, (TournamentStarted,))
U->>P: repository.save(new)
U->>E: publish(events) (recorded)
U-->>R: Tournament
R-->>C: 200 TournamentResponse.from_domain(...)
Note over P,E: commit, then the handlers run - never before
src/cleanarch/
├── shared/ TEMPLATE building blocks: DomainEvent, DomainResult, errors,
│ EventPublisher port, DB session, in-process bus, HTTP error mapping
├── tournaments/ EXAMPLE one feature, four rings:
│ ├── domain/ Tournament aggregate, phases, progress, events, rules
│ ├── application/ use cases, TournamentRepository port, command, authorization
│ ├── infrastructure/ in_memory.py · sqlalchemy/{models,mapping,repository}.py
│ ├── http/ router, schemas, use-case factories
│ └── cli/ the same use cases from the terminal
├── bootstrap/ TEMPLATE settings · logging · transaction middleware · app.py · cli.py
├── main.py uvicorn cleanarch.main:app
└── __main__.py python -m cleanarch
tests/
├── domain/ pure rules, no I/O ms
├── application/ use cases with the in-memory adapter ms
├── integration/ repository contract: in-memory + SQL ~100 ms
├── api/ HTTP boundary, auth, ops, HTTP→DB on SQLite ms
├── cli/ command line on a SQLite file ms
└── architecture/ the Dependency Rule, executable ms
A tournament is a sequence of phases (Swiss/round-robin rounds, single/double elimination brackets), each after the first preceded by a top-N cut. It can be created, started and advanced round by round until it finishes. Real invariants, real state machine, real events - readable in five minutes.
Glossary, if you don't play tournaments
- Swiss / round-robin - pairing systems for round-based phases: everybody plays a fixed number of rounds.
- Bracket - knock-out phase: single elimination (lose once, out) or double.
- Cut - who advances into a later phase, e.g. top 8. The first phase has no cut; every later phase needs one.
- Progress - where the tournament is: not started, phase i / round j, or finished.
curl -s -X POST localhost:8000/api/v1/tournaments -H 'content-type: application/json' -d '{
"name": "Spring Cup",
"phases": [
{"config": {"kind": "round", "rounds": 2, "pairing": "swiss"}},
{"config": {"kind": "bracket", "elimination": "single"}, "cut": {"players": 8}}
]}'
curl -s -X POST localhost:8000/api/v1/tournaments/<id>/start
curl -s -X POST localhost:8000/api/v1/tournaments/<id>/advance
curl -s -X POST localhost:8000/api/v1/tournaments/<id>/start
# → 422 {"error": "TournamentAlreadyStarted", "message": "Tournament has already started."}The whole use case:
class StartTournament:
def __init__(self, repository: TournamentRepository, events: EventPublisher) -> None:
self._repository = repository
self._events = events
async def execute(self, tournament_id: TournamentId) -> Tournament:
tournament = await _require(self._repository, tournament_id)
result = tournament.start() # DomainResult: new aggregate + events
await self._repository.save(result.aggregate)
await self._events.publish(result.events)
return result.aggregateuv run python scripts/init_project.py --name shopapi --remove-example # your package, no example,
# no template-only checks
make check # green
uv run python scripts/new_feature.py orders # a wired, tested feature
make check # still greeninit_project.py leaves your project: the example, the proof system and this README are
gone; the dependency rule stays as a tool in make check, and the mechanisms you inherit
stay tested. new_feature.py generates a vertical slice - entity, event, port, use cases,
in-memory adapter, router - wires it in bootstrap/features/orders.py and writes its first
tests. This whole journey runs as a test on every push (tests/template/test_user_journey.py).
| Decision | Why | Add it when... | ADR |
|---|---|---|---|
| Feature-first layout, rings inside | delete a feature = delete a folder; rings still enforced per feature | - | 001 |
| No DI library | Depends + dependency_overrides in one composition root; nothing to learn |
the object graph gets deep | 002 |
| No mediator / CQRS bus | use cases are classes you call; greppable | several entrypoints need one pipeline | 003 |
| Transaction per request, no UoW | session opened/committed by the HTTP layer; use cases stay 4 lines | two aggregates per operation, or non-HTTP entrypoints | 004 |
| Events in-process, after commit | values returned by domain methods, dispatched once the transaction committed; at-most-once | a handler must survive a crash or be retried (outbox) | 005 |
| FastAPI + SQLAlchemy 2 + Alembic + uv + Ruff + mypy | mainstream, typed, confined to the outer rings | - | 006 |
| Shape in Pydantic, rules in the domain | one source of truth per rule, on every entry path | - | 007 |
| Separate row model + explicit mapping | frozen dataclass domain; table evolves independently | - | 008 |
| Auth is an adapter, authorization a rule | API key → Actor; use cases decide; domain records organizer_id |
JWT/OAuth: swap one file | 009 |
| Optimistic concurrency on every adapter | version checked on save; stale write → 409, never a lost update |
- | 010 |
make architecture |
the dependency rule as a tool (scripts/archcheck.py check), part of make check |
make proof |
(template repository) the executable guarantees, one verdict each (~20 s) |
make graph |
regenerate the dependency graph page from the code |
make test / make test-fast |
full suite with coverage / without the repository contract tests |
make lint / make format / make typecheck |
Ruff, Ruff --fix, mypy --strict |
make migrate / make migration m="..." |
Alembic upgrade / autogenerate |
make docs |
Docusaurus dev server |
docker compose up |
API + PostgreSQL 17 |
.githooks/ / .pre-commit-config.yaml |
commit-msg enforces Conventional Commits; pre-commit runs the fast checks (bash hooks or the pre-commit framework, your choice) |
.github/workflows/ |
lint · tests on 3 OS × 2 Pythons · Postgres integration · Docker smoke test · docs · semantic-release · GHCR image |
Releases are automatic: Conventional Commits on main → version, CHANGELOG.md, tag, GitHub
release and ghcr.io/<owner>/<repo>:X.Y.Z. See CONTRIBUTING.md.
We benchmarked the popular Python Clean Architecture / DDD repositories and the most-starred templates in other ecosystems before settling the structure. The short version is in the docs; the one-line version:
Boundaries verified by tests that run in CI, an example domain removed with one command, and a written reason for every pattern present - or absent. We have not found another Python template that does all three; if you know one, open an issue and we will link it.
Issues and PRs are welcome. Read CONTRIBUTING.md first - the bar is "does it make the architecture clearer for the next person who forks it?".
MIT. Free to use, fork and ship. If it saved you time, a star helps others find it.