Skip to content

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.

DepsRegistry and LifecyclePlan feed the ExecutionRuntime, which creates an ExecutionContext per scope DepsRegistry and LifecyclePlan feed the ExecutionRuntime, which creates an ExecutionContext per scope

Two registries, frozen

You describe the runtime with two registries, then freeze them — the runtime takes frozen, validated inputs and does not coerce them:

  • DepsRegistrywhat to build: which adapters back which specifications.
  • LifecyclePlanstartup 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 ...
  1. Create context — resolve the frozen deps into live ports.
  2. Startup — run lifecycle startup steps in dependency order.
  3. Serve — the application handles requests.
  4. Drain — stop admitting new invocations; give in-flight operations a bounded window to finish (see Shutdown & fleets).
  5. Shutdown — run shutdown steps in reverse.
  6. 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.

  • Quickstart


    A running service in about ten minutes, in-memory — no Docker.

  • CRUD over Postgres


    Wire real backends: Postgres, Redis, FastAPI, and more.