Runtime
The runtime turns a pile of specifications, operations, and adapters into a running service. It owns three things: the dependency wiring, the lifecycle of infrastructure clients, and the per-request execution context.
Two registries, frozen¶
You describe the runtime with two registries, then freeze them — the runtime takes frozen, validated inputs and does not coerce them:
DepsRegistry— what to build: which adapters back which specifications.LifecyclePlan— startup and shutdown: opening pools, warming caches, and closing connections, in dependency order.
from forze.application.execution import (
DepsRegistry,
ExecutionRuntime,
LifecyclePlan,
)
runtime = ExecutionRuntime(
deps=DepsRegistry.from_modules(postgres_module, redis_module).freeze(),
lifecycle=LifecyclePlan.from_modules(postgres_lifecycle).freeze(),
)
The execution context¶
The ExecutionContext is the seam from the overview: the one
object handlers use to resolve every port. It also carries request identity — who
is calling (AuthnIdentity), which tenant (TenantIdentity), and correlation
metadata — bound at the application boundary, for example in HTTP middleware.
The scope lifecycle¶
A runtime runs inside a scope. Entering it builds the context and starts infrastructure; leaving it tears everything down in reverse.
async with runtime.scope():
ctx = runtime.get_context()
# ... handle requests ...
- Create context — resolve the frozen deps into live ports.
- Startup — run lifecycle startup steps in dependency order.
- Serve — the application handles requests.
- Drain — stop admitting new invocations; give in-flight operations a bounded window to finish (see Shutdown & fleets).
- Shutdown — run shutdown steps in reverse.
- Reset — clear the context.
No scope, no context
Resolving a port outside an active scope fails. Wire runtime.scope() (or
startup() / shutdown()) into your framework's lifespan — see the
Quickstart.
If a startup step fails, the steps that already ran are shut down in reverse before the error propagates — no half-open pools left behind.
Transactions¶
Multi-step writes that must commit together run in a transaction scope, keyed by the same specification name as the deps wiring:
async with ctx.tx_ctx.scope("orders"):
await doc.create(...)
await outbox.stage(...)
Nested scopes reuse the same transaction, with savepoints where the backend supports them.
You've got the model¶
That's the whole shape of Forze: aggregates at the center, operations over them, ports out to infrastructure, and a runtime to tie it together. Time to build something with it.
-
A running service in about ten minutes, in-memory — no Docker.
-
Wire real backends: Postgres, Redis, FastAPI, and more.