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/ — pure business core"]
        MD["models"]
        PT["ports (abstract)"]
        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 or a database driver.

Legend — solid arrows are compile-time calls; dotted arrows are the port/adapter contract, resolved 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/ nothing but its own ports web frameworks, drivers, infra
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 an abstract port; the composition root injects a concrete adapter.
  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