Overview
The blueprint is a hexagonal (ports & adapters) application with a spec-first web edge and a single composition root. It favors explicit wiring over implicit magic and existing internal libraries over hand-written infrastructure. Start here to build the mental model; the enforceable details live in the reference pages.
🏗️ Architecture
flowchart LR
subgraph EDGE["interfaces/ — HTTP edge"]
OAS["openapi.yaml<br/>operationId contract"]
RT["<resource>/routes.py<br/>thin handlers"]
end
subgraph CORE["domain/ — business core"]
MD["models"]
PT["ports (adapter binding seam)"]
SV["services"]
end
subgraph INFRA["infra/ — adapters"]
RP["mongo repos (mongo-ops)"]
AD["cache · stores · llm · mail"]
DB[("MongoDB")]
end
CR["init.py<br/>composition root"]
OAS --> RT
RT --> SV
SV --> PT
SV --> RP
PT -. depends on .- RP
CR --- RT
CR --- SV
CR --- RP
RP --> DB
AD --- CR
The rule holds at every edge: code in an outer ring may depend on a ring closer
to the core, never the reverse. domain/ never imports a web framework;
only its ports/ seam reaches into infra/ to bind adapters.
Legend — solid arrows are compile-time calls; dotted arrows are the port/adapter binding, performed by the composition root at startup.
🔄 The one-way dependency rule
| Ring | May import | Never imports |
|---|---|---|
interfaces/ |
domain, framework glue (glue only) |
business logic, infra directly |
domain/ |
its own models + its ports/ seam (which binds infra/libraries) |
web frameworks, interfaces/ |
infra/ |
domain ports, drivers, libraries |
— |
init.py |
everything (the wiring point) | — |
🧭 Core principles
- One-way dependencies.
interfaces -> domain -> infrais allowed; the reverse is forbidden. - Ports in the domain, adapters in infra. A service depends on a port; the port's factory binds the concrete adapter, and the composition root calls those factories.
- One composition root. Nothing constructs its own dependencies. All
wiring lives in one module (
init.py) exposed as a flat resource registry. - Spec-first. The HTTP contract is declared in
openapi.yaml; route handlers are looked up byoperationId. There are no decorator routers. - Reuse the libraries. Orchestration, DI, Mongo repositories, ingestion, auth, and docs come from internal libraries — see the Library Registry.
- Explicit state machines. Long-running work moves through explicit, durable statuses and can pause for human input.
⚙️ Runtime processes
A standard app may run as up to three processes that share the same composition root:
| Process | Entry point | Responsibility |
|---|---|---|
| API | main.py |
serve HTTP; start/stop resources via lifespan |
| Worker | worker.py |
poll durable jobs, run pipelines, publish progress |
| Scripts | python -m scripts.<name> |
one-shot operator tasks |