Skip to content

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

  1. One-way dependencies. interfaces -> domain -> infra is allowed; the reverse is forbidden.
  2. 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.
  3. One composition root. Nothing constructs its own dependencies. All wiring lives in one module (init.py) exposed as a flat resource registry.
  4. Spec-first. The HTTP contract is declared in openapi.yaml; route handlers are looked up by operationId. There are no decorator routers.
  5. Reuse the libraries. Orchestration, DI, Mongo repositories, ingestion, auth, and docs come from internal libraries — see the Library Registry.
  6. 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