Skip to content

App Components

What ships in every standard app, and how the pieces relate. This is the blueprint's component model — templates/app/ is a runnable instance of it.


🗂️ Component model

The blueprint has two orthogonal views: runtime flow (who calls whom) and composition-root wiring (who constructs whom). Keeping them separate makes the layering visible instead of hiding it behind a hub.

🔀 Runtime flow

Who calls whom at request and job time:

%%{init: {"flowchart": {"nodeSpacing": 50, "rankSpacing": 80}}}%%
flowchart TB
    subgraph REQ["Request path — API process"]
        direction LR
        CL(["Client<br/>operationId"]) --> RT["interfaces/&lt;resource&gt;/routes.py"]
        RT --> SVC["&lt;resource&gt;_svc"]
        SVC --> REPO["&lt;resource&gt;_repo"]
    end

    subgraph WRK["Background path — worker process"]
        direction LR
        W["worker.py"] --> JSVC["job_svc"]
        W --> PIPE["pipelines/&lt;family&gt;"]
        PIPE --> ISVC["item_svc"]
        JSVC --> JREPO["job_repo"]
        ISVC --> IREPO["item_repo"]
    end

    subgraph INFRA["infra/ — adapters"]
        MONGO[("MongoDB / mongo_ops")]
    end

    REPO --> MONGO
    JREPO --> MONGO
    IREPO --> MONGO

Legend — solid arrows are runtime calls. Nothing is self-constructed.

🧵 Composition-root wiring

Who constructs what at startup:

%%{init: {"flowchart": {"nodeSpacing": 50, "rankSpacing": 80}}}%%
flowchart TB
    INIT["init.py — composition root"]
    SETUP["infra/db/setup.py"]
    FACT["domain/*/ports/*<br/>factory seams"]
    REPOS["repos<br/>infra/db/repos/mongo.py"]
    SVCES["services<br/>CrudsServiceAdaptor"]
    STATE["app.state.db_resources"]
    HEXA["hexa.build"]
    RT["routes.py"]
    WK["worker.py"]

    INIT --> SETUP
    INIT --> FACT
    FACT --> REPOS
    FACT --> SVCES
    REPOS --> SVCES
    HEXA -.-> SVCES
    INIT --> STATE
    STATE --> RT
    INIT --> WK

Legend — init.py is the only place that imports concrete adapters. The ports/ factory seams bind infra; the API reads resources from app.state.db_resources, the worker calls init() directly.


📦 Component roles

1. interfaces/ — the edge

Thin handlers bound to openapi.yaml operationIds. They parse the request, call a service, and shape the response. No business rules, no I/O.

  • interfaces/openapi.py re-exports every route module so the OpenAPI-first app can resolve handlers.
  • Routes may mount the cache decorator on read endpoints.

2. domain/ — the core

Models, ports, and services (use cases). No framework imports. The ports/ package is the one seam allowed to reach into infra/: each port module declares the document shape and a factory that binds the adapter.

  • <aggregate>/models.py — dataclasses, enums, document shapes.
  • <aggregate>/ports/db.py — document model + create_<aggregate>_repo().
  • <aggregate>/ports/service.py — service + get_<aggregate>_service(repo).
  • Services compose ports and enforce the business rules; the port factories bind the implementations, and the composition root calls them.

3. infra/ — the adapters

Concrete implementations of the domain ports plus cross-cutting infrastructure:

  • db/ — mongo-ops repositories and the connection lifecycle.
  • adapters/ — cache store, progress bus, storages, fetchers (library-backed).
  • Anything else the domain needs, implemented against its ports.

4. pipelines/ — data-driven families (optional)

Extraction and enrichment families, each a deterministic step chain (abc.py / impl.py / pipeline.yaml / resources/). Roots are registered in the pattern registry and driven either by a worker job or a CLI.

5. worker.py — the job worker (optional)

A separate process that polls durable job collections, runs pipeline roots, and publishes progress to the progress bus. It shares the same composition root as the API.

6. init.py — the composition root

The only place anything is constructed: leaf repositories → dependent repositories → services → adapters. Exposes get_svc overloads and starts / stops resources on the app lifespan.


🔗 How the request flows

sequenceDiagram
    participant C as Client
    participant R as route (interfaces)
    participant S as service (port factory / adapter)
    participant P as repo (infra)
    participant DB as MongoDB

    C->>R: POST /items (operationId)
    R->>S: svc.create(payload)
    S->>P: repo.insert(doc)
    P->>DB: insert
    DB-->>P: ack
    P-->>S: doc
    S-->>R: result
    R-->>C: 200 JSON