Project Architecture Decision Checklist
Use this checklist when starting or restructuring a Python service so layout, boundaries, and tooling decisions are made deliberately before code hardens around accidents.
How to Use This Checklist
- Walk tiers in order - later choices depend on earlier ones.
- Record each decision in a short ADR or README section so the team does not re-litigate defaults.
- Revisit after the first production deploy; greenfield assumptions rarely survive contact with users.
- Treat unchecked items as debt with an owner and target date, not silent gaps.
Foundation Decisions
- Runtime pin: Python 3.14.0 (or your org's supported floor) locked in
pyproject.toml, CI, and Docker images.- 3.14+: greenfield services, latest typing and stdlib features.
- 3.13 maintenance: libraries that must support older deploy targets.
- Package manager:
uv 0.6+for lockfiles and fast installs vs plainpip+requirements.txt.- uv: teams wanting reproducible envs and workspace monorepos.
- pip: minimal scripts with no lockfile requirement.
- Layout:
src/layout vs flat package at repo root.- src layout: libraries and services tested as installed packages.
- flat: tiny one-file utilities only.
- Distribution model: installable package vs deployable app (or both).
- Package: shared library consumed by multiple services.
- App: single deployable with a
mainentry point.
- Monorepo vs polyrepo: one repo with multiple packages vs one service per repo.
- Monorepo: shared domain models and coordinated releases.
- Polyrepo: independent teams and release cadences.
Boundary Decisions
- Domain layer: pure Python module(s) with no framework imports.
- Yes: long-lived business rules outlive FastAPI/Django/Flask.
- No: throwaway scripts and notebooks.
- HTTP framework: FastAPI 0.115+, Django 5.2, or Flask 3.1.
- FastAPI: async APIs, OpenAPI-first, Pydantic 2 models.
- Django: admin, ORM, batteries-included web apps.
- Flask: minimal WSGI apps and gradual growth.
- Persistence boundary: repository/DAO layer vs ORM calls sprinkled in handlers.
- Repository: testable domain without a database.
- Direct ORM: prototypes and internal tools.
- Configuration source: environment variables +
pydantic-settingsvs config files checked into git.- Env + pydantic-settings: 12-factor deploys across dev/stage/prod.
- Files: local-only dev tools with no secrets.
- Secrets handling: platform secret store vs
.envexcluded from git.- Store: production credentials never on disk in the image.
.env: local developer machines only.
Quality & Tooling
- Linter/formatter:
ruff 0.9+as the default all-in-one tool. - Type checking: mypy or pyright in CI with a ratchet strategy (new code strict, legacy gradual).
- Test runner:
pytestwith fixtures colocated undertests/. - Coverage gate: minimum threshold on changed lines, not a vanity 100% goal.
- Pre-commit hooks: format, lint, and type-check before push.
- CI matrix: test on pinned Python plus one maintenance version (e.g. 3.13).
Data & Integration
- Tabular data stack: pandas 2.2+ vs Polars 1.x.
- pandas: widest ecosystem and notebook familiarity.
- Polars: performance-critical ETL and lazy pipelines.
- ML inference boundary: PyTorch 2.6+ loaded in an adapter, not inside domain entities.
- External HTTP clients: dedicated client module with timeouts, retries, and typed responses.
- Message/async work: background tasks in-process vs queue worker (Celery, ARQ, Dramatiq).
Operations
- Logging: structured JSON logs with correlation IDs, not
printin library code. - Health endpoints: liveness vs readiness separated for orchestrators.
- Container entrypoint: explicit
python -m packageor uvicorn/gunicorn command documented. - Migration strategy: Alembic/Django migrations run as a deploy step, not manual SQL.
- Observability: metrics and traces at framework middleware boundary, not inside domain functions.
Applying the Checklist in Order
- Foundation (1-5): hardest to reverse - decide before the first merge.
- Boundaries (6-10): sets testability and framework swap cost.
- Quality (11-16): cheap to add early, expensive to retrofit.
- Data/Ops (17-25): align with team skills and production platform.
FAQs
Should a new API project default to FastAPI or Django?
Choose FastAPI when the primary surface is a JSON/HTTP API with OpenAPI docs and async I/O. Choose Django when you need the admin, auth ecosystem, and ORM as one cohesive stack. Either can grow - but switching later is costly.
Is src layout worth it for a small service?
Yes for anything installed in CI or Docker. src layout forces tests to import the package the way production does, catching "works locally because PYTHONPATH hack" bugs early.
When can I skip a dedicated domain layer?
Skip it for scripts under ~300 lines with no expected lifespan beyond a quarter. Once multiple endpoints share rules, extract domain logic before duplication wins.
Do I need both pandas and Polars?
Usually no. Pick one primary tabular engine per service. Use the other only at integration boundaries if a partner pipeline requires it.
Where do Pydantic models belong?
At boundaries: HTTP request/response DTOs, settings, and external API shapes. Keep core domain types as dataclasses or plain classes unless validation is inherently boundary-focused.
How strict should type checking be on day one?
Enable checking in CI immediately but allow overrides in legacy modules. Ratchet: new files must pass strict rules; tighten global settings each sprint.
Should secrets live in pydantic-settings?
Settings classes should read secrets from the environment or a secret backend. Never commit secret values; document required variable names in .env.example.
Monorepo or polyrepo for two related services?
Monorepo when they share models and release together. Polyrepo when teams, SLAs, and deploy permissions are independent.
What is the minimum CI matrix for Python 3.14?
Run tests on 3.14.0 plus 3.13 if you ship libraries. Application-only repos may pin a single version to match production images.
When should I write an ADR for a checklist item?
Write a short ADR when the decision affects multiple teams, is hard to reverse, or surprised someone during review. A one-line README note is enough for obvious defaults like "we use ruff."
How do I record unchecked items?
Add a tracked issue per gap: owner, risk level, and whether it blocks production. Review in sprint planning instead of leaving silent unchecked boxes.
Does this checklist apply to notebooks and ML experiments?
Use the Foundation and Quality tiers. Boundary and Ops tiers apply when an experiment promotes to a scheduled job or API.
Related
- src Layout & Package Structure - import hygiene after layout choice
- Configuration & Settings - env-based config patterns
- Clean / Hexagonal Architecture - ports and adapters detail
- Refactoring Decisions - when the wrong early choice needs correction
- Architecture Best Practices - ongoing design habits
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.