System Architecture¶
openframe-core is a pure Python foundation package built around a unified
port + lifecycle contract layer (ADR-006). ports/ is the apex module
— every other module depends on it, directly or transitively — and both
the outbound (ports/outbound/) and inbound (inbound/) sides of the
hexagon are first-class citizens of the dependency graph. No module
imports from a module higher in the chain.
As of v3.1.0, ports/ unifies what were previously two separate
modules: openframe.core.contracts (the port/lifecycle primitives) and
openframe.core.ports (the outbound protocols, now internally organised
under ports/outbound/). See
ADR-006's v3.1.0 addendum
for the full rationale.
Module Dependency DAG¶
Every arrow in this diagram is a permitted import direction. No reverse
imports exist. There is no standalone health node — health is a member
of ports (Lifecycle.health() + PluginHealth). There is no
standalone errors node either — the whole error hierarchy (the adapter
family and the plugin family) lives in the single exceptions package,
rooted at OpenFrameError. telemetry imports exceptions so its
record_error seam can read an error's structured data without the error
ever importing telemetry.
flowchart LR
EX["exceptions"]
CF["config"]
PO["ports\n(+ ports/outbound)"]
IN["inbound"]
TE["telemetry"]
TR["tracing"]
MW["middleware"]
PL["plugins"]
RT["runtime"]
TS["testing"]
EX --> CF --> PO
EX --> TE
PO --> IN
PO --> TE --> TR --> MW
PO --> PL --> RT
TE --> PL
PO --> TS
style EX fill:#1a1a1a,color:#F0F0F0,stroke:#6DB33F
style CF fill:#1a1a1a,color:#F0F0F0,stroke:#6DB33F
style PO fill:#1a1a1a,color:#8CC63F,stroke:#6DB33F
style IN fill:#141414,color:#F0F0F0,stroke:#4E8A2A
style TE fill:#141414,color:#F0F0F0,stroke:#4E8A2A
style TR fill:#141414,color:#F0F0F0,stroke:#4E8A2A
style MW fill:#141414,color:#F0F0F0,stroke:#4E8A2A
style PL fill:#141414,color:#F0F0F0,stroke:#4E8A2A
style RT fill:#141414,color:#F0F0F0,stroke:#4E8A2A
style TS fill:#141414,color:#F0F0F0,stroke:#4E8A2A
ports/outbound/ is drawn as part of the ports node, not a sibling —
it is an internal sub-package (repository.py, producer.py,
consumer.py, each depending only on ports/port), not a separately
importable module. Consumers always import from the top-level
openframe.core.ports package.
See ADR-006 for the full
rationale, and capability-taxonomy.md for the
Capability enum reference.
Module Inventory¶
| Module | Public exports | External deps |
|---|---|---|
exceptions |
OpenFrameError (root), ErrorCode, Severity, AdapterError + 5 subclasses, PluginError + 4 subclasses (incl. AmbiguousCapabilityError) |
none |
config |
BaseAdapterSettings |
pydantic-settings |
ports |
Identity, Lifecycle, BasePort, Capability, PluginStatus, PluginHealth, PluginContext, PrincipalContext, TenantContext (primitives) + BaseRepository[T], BaseProducer[T], BaseConsumer[T] (outbound protocols, defined in ports/outbound/, each BasePort + domain methods) |
none |
inbound |
UseCase[TIn, TOut], CommandHandler[TIn], QueryHandler[TIn, TOut], RequestContext |
none |
telemetry |
setup_telemetry, get_tracer, get_meter, record_lifecycle_event, record_error |
opentelemetry-* |
tracing |
TracingProxy |
opentelemetry-api |
middleware |
TelemetryMiddleware, ASGIScope, ASGIMessage, Receive, Send, ASGIApp |
opentelemetry-api |
plugins |
PluginRegistry (+ re-exports of ports primitives) |
none |
runtime |
ApplicationBootstrap |
none |
testing |
InMemoryRepository, FakeProducer, FakeConsumer, LifecycleContractTests, PortContractTests, RepositoryContractTests, ProducerContractTests, ConsumerContractTests |
none |
The Hexagon, Explicitly¶
ports sits at the apex, and now contains both sides of the hexagon's
outbound half in one module:
- Port primitives (
ports/capability.py,context.py,health.py,identity.py,lifecycle.py,port.py) —Identity+Lifecyclecompose intoBasePort, the base every outbound port and every registrable plugin extends. ports/outbound/— the outbound/driven side proper. Every capability-specific protocol (BaseRepository,BaseProducer,BaseConsumer) isBasePortplus its own domain methods. Adapters implement these structurally. New outbound capabilities (e.g.BaseSecretsProviderfromopenframe-infra) are added here.
inbound is the sibling module for the driving side: UseCase/
CommandHandler/QueryHandler are invoked by inbound adapters (HTTP
routes via TelemetryMiddleware, message handlers, CLI commands) with a
RequestContext carrying a correlation id and optional identity.
plugins.PluginRegistry operates directly on BasePort — there is no
separate plugin protocol. A "plugin" is just a registered BasePort.
How a Template Uses openframe-core¶
The following sequence shows how a FastAPI template wires openframe-core
at startup and serves a request.
sequenceDiagram
participant L as lifespan handler
participant D as deps.py
participant Reg as PluginRegistry
participant TP as TracingProxy
participant R as PostgresRepository
participant MW as TelemetryMiddleware
L->>L: setup_telemetry()
L->>D: build_repository()
D->>R: PostgresRepository(settings)
D->>Reg: registry.register(repo, config=pg_config)
Reg->>R: repo.initialize(PluginContext)
D->>TP: TracingProxy(repo, "repository.item")
D-->>L: traced, initialized repository ready
Note over MW: every HTTP request
MW->>MW: start span "HTTP GET /items/{id}"
MW->>D: registry.get(Capability.PERSISTENCE) → TracingProxy
D->>TP: repo.get(entity_id)
TP->>TP: start child span "repository.item.get"
TP->>R: PostgresRepository.get(entity_id)
R-->>TP: entity
TP-->>D: entity
D-->>MW: entity
MW->>MW: set span status OK, record metrics
MW-->>MW: inject x-session-id, emit log
Architecture Scorecard — v3.0.0¶
Scores reflect the state of openframe-core after the v3.0.0 release.
Each dimension is rated /10. "Why it moved" is the concrete reason the
score changed from the pre-v3 baseline.
| Dimension | Pre-v3 | v3.0.0 | Why it moved |
|---|---|---|---|
| Contract design | 9.0 | 9.5 | Capability enum closes the open-string gap; BasePort unification removes the dual-mechanism ambiguity (lifecycle-free port + separate OpenFramePlugin). |
| Error model | 9.0 | 9.5 | StrEnum + decentralised domain.kind convention is the right extensibility model for a multi-package ecosystem — downstream packages declare their own codes without touching the core enum. |
| Testing infrastructure | — | 9.0 | testing/contracts/ and testing/fakes/ centralise what every adapter was duplicating independently. Highest-leverage addition for the ecosystem as a whole. |
| Composition / wiring | 5.0 | 7.0 | ApplicationBootstrap names the third composition option. Held back: shutdown_telemetry() is still absent — lifecycle is not complete until the OTel SDK is flushed and shut down on process exit (see open gap below). |
| Telemetry | 7.0 | 8.0 | Sidecar architecture, bounded-queue metric export, and record_error() seam are the right design. Held back: no shutdown flush hook yet — spans are silently dropped on every normal process shutdown. |
Resolved gaps (post-v3.0.0)¶
Both gaps identified in the v3.0.0 scorecard review have been resolved:
Gap 1 — shutdown_telemetry() ✓
openframe.core.telemetry.shutdown_telemetry() now flushes and shuts
down both the TracerProvider (forcing BatchSpanProcessor export) and
the MeterProvider before returning. ApplicationBootstrap.stop() calls
it automatically. Templates that do not use ApplicationBootstrap should
call it in the lifespan teardown path.
Gap 2 — openframe.core.tracing.propagation ✓
openframe/core/tracing/propagation.py is now shipped. Exposes
inject(carrier) and extract(carrier) as named wrappers around the OTel
propagator API — all broker adapters (Kafka, NATS, RabbitMQ, …) import
from this single module.
Deployment Topology¶
openframe-core is a library — it has no runtime process. It is installed
as a dependency inside a Modal function container (or any other Python
environment) and runs in-process with the application.
flowchart TD
PyPI["PyPI\nopenframe-core 3.1.0"]
Container["Modal function container\npip install openframe-core"]
App["FastAPI application\nfrom openframe.core.* import ..."]
OTel["OTLP endpoint\nGrafana Cloud / Honeycomb / Datadog"]
PyPI --> Container
Container --> App
App -->|traces + metrics| OTel
style PyPI fill:#1a1a1a,color:#F0F0F0,stroke:#6DB33F
style Container fill:#141414,color:#F0F0F0,stroke:#4E8A2A
style App fill:#1a1a1a,color:#8CC63F,stroke:#6DB33F
style OTel fill:#141414,color:#F0F0F0,stroke:#4E8A2A