Architecture¶
This document explains FastDI’s architecture: the Rust core, the Python API layers, dependency planning, scopes, overrides, observability, and typing.
Overview¶
- Core: Rust crate built with PyO3 provides a high‑performance provider store, resolution, overrides, and a synchronous plan executor.
- Python: thin, readable layers on top of the core provide ergonomics:
fastdi.types: type aliases,Depends, key utilities, dependency parsing (Annotated-only).fastdi.container:Containerwith scopes, hooks, async resolver, topological planning, and epoch‑based plan invalidation.fastdi.decorators:provide,inject,ainjectdecorators for easy use.- Packaging: maturin builds a CPython extension;
_fastdi_core.pyiandfastdi/py.typedship editor/type checker hints.
Rust Core¶
The Rust library (src/lib.rs) exposes a Container class to Python via PyO3.
It encapsulates two main data sets:
providers: HashMap<String, Provider>— base registration tableoverrides: Vec<HashMap<String, Provider>>— layered overrides (stack)
Each Provider holds:
callable: Py<PyAny>— the Python factory functionmeta: ProviderMeta— flags and static metadatasingleton: bool— whether to cache the produced value globally in Rustis_async: bool— async providers are rejected by sync pathsdep_keys: Vec<String>— dependency keyscache: Option<Py<PyAny>>— singleton cache (ifsingleton=True)
Resolution (sync)¶
- Recursive resolver:
resolve_keywalks dependencies with a DFS, detects cycles using aseenset, and calls providers, caching singletons when set. - Batch resolve:
resolve_manysimply iterates over keys and calls the above.
Plan executor (sync)¶
For faster sync paths without recursion:
compile_order(roots): builds a topological order via DFS, rejects async providers and cycles, and records dependencies per node.execute_order(order): iteratively computes each node once, looking up already computed dependencies, reusing singleton caches, then returns the computed map.- PyO3 API:
resolve_many_plan(keys)compiles + executes and returns values in the order of input keys.
Sequence for sync injection:
sequenceDiagram
participant User as @inject wrapper
participant Py as Container (Python)
participant Rs as Container (Rust)
User->>Py: call handler()
Py->>Py: validate plan epoch
Py->>Rs: resolve_many_plan(dep_keys)
Rs->>Rs: compile_order(dep_keys)
Rs->>Rs: execute_order(order)
Rs->>Py: values
Py->>User: call original func(values)
Overrides¶
Overrides use a stack of hash maps. Lookups search the latest override layer first, then fall back to base providers. Singleton caches are kept with the provider entry (override or base), ensuring isolation across layers.
Dependency Graph Examples¶
Typical acyclic graph (valid):
flowchart TD
A[get_db] --> B[get_service]
B --> H[handler]
A --> H
Graph with a cycle (invalid):
flowchart TD
X[a] --> Y[b]
Y --> X
FastDI detects cycles during planning (Python _build_plan) and at runtime in Rust’s recursive path, raising a clear error.
Python Layers¶
Types and helpers (fastdi.types)¶
Key,Scope,Hook: public type aliases.make_key(obj): stable string key for callables/strings/objects.Depends(target): declarative dependency marker for function parameters.extract_dep_keys(func): parsesDependsmarkers fromtyping.Annotated[..., Depends(...)](Annotated-only style).CoreContainerProto: Protocol describing the PyO3Containerinterface to satisfy editors and type checkers.
Container (fastdi.container)¶
Responsibilities:
- Registration:
register(key, func, *, singleton, scope, dep_keys=None) - Detects coroutine functions (
is_async) and passes metadata to the core. - Tracks Python‑side scopes:
transient,request, orsingleton. - Overrides:
override(key_or_callable, replacement, *, singleton=False) - Creates a top override layer, registers replacement, then pops the layer on exit. Bumps an internal epoch counter on entry and on exit.
- Scopes:
singleton: cached in Rust provider entries.request: cached per async task in aWeakKeyDictionarywith a ContextVar fallback for non‑task contexts.transient: no caching.- Hooks:
add_hook,remove_hook, internal_emit(event, payload)withprovider_start,provider_end, andcache_hitevents. - Async resolution:
_resolve_key_asyncmirrors Rust logic, awaits async providers, applies request caching, and emits hook events. - Planning:
_build_plan(root_keys, allow_async): creates a topological order by querying the core for per‑key metadata; rejects cycles and async providers ifallow_async=False._run_plan_async(plan): iterative executor for async graphs in Python honoring request/singleton caches and emitting events.
Async execution sequence:
sequenceDiagram
participant User as @ainject wrapper
participant Py as Container (Python)
participant Core as _fastdi_core (meta)
User->>Py: await handler()
Py->>Py: check epoch, rebuild plan if needed
loop for key in plan.order
Py->>Py: request/singleton cache lookup
alt miss
Py->>Core: get_provider_info(key)
Py->>Py: await deps from computed
Py->>Py: call provider (await if async)
Py->>Py: update caches, computed
end
end
Py->>User: await original func(values)
- Epoch invalidation:
_epochcounter increments on registration and on entering/exiting anoverridecontext. Decorators rebuild or revalidate plans if the epoch changed since the last run.
Decorators (fastdi.decorators)¶
provide(container, *, singleton=False, key=None, scope=None)- Registers the decorated function and returns it unchanged.
inject(container)(sync)- Compiles/validates a plan at decoration, stores the container epoch, and on
each call revalidates when the epoch changes. Executes via the Rust plan
executor (
resolve_many_plan) for performance. ainject(container)(async)- Compiles a plan at decoration, stores epoch, rebuilds when the epoch
changes, executes iteratively in Python (
_run_plan_async), and awaits the original function.
Typing and Tooling¶
_fastdi_core.pyi: PEP‑561 stub describing the PyO3 extension’sContainer.fastdi/py.typed: marker so type checkers use inline hints.- Protocol
CoreContainerProtogives editors accurate method signatures. - Decorators are typed to return zero‑arg wrappers with the original return type, matching the runtime injection semantics.
- High-level components:
flowchart LR
subgraph Python
A["fastdi.types<br/>Depends, extract_dep_keys"]
B["fastdi.container<br/>Container, async resolver, plan"]
C["fastdi.decorators<br/>provide, inject, ainject"]
end
D["PyO3 extension<br/>_fastdi_core.Container<br/>(Rust)"]
A --> B
B --> C
B <--> D
C --> B
Observability¶
Hooks receive structured events:
provider_start:{key, async}provider_end:{key, async, duration_s}cache_hit:{key, scope}
Payloads can be consumed by loggers or metrics collectors. Hook failures are ignored to avoid disrupting resolution.
Example event flow (simplified):
sequenceDiagram
participant C as Container
participant H as Hook
C->>H: provider_start {key, async}
C->>H: provider_end {key, async, duration_s}
C->>H: cache_hit {key, scope}
Error Handling¶
- Missing provider →
KeyError(RustPyKeyError). - Cycles →
RuntimeError(RustPyRuntimeErrorand Python runtime errors). - Async providers in sync paths →
RuntimeErrorwith a clear message.
Packaging and Local Dev¶
- Built with maturin (
pyproject.toml), exposed module:_fastdi_core. - Local workflow with
uv: uv venv .venv && . .venv/bin/activateuv pip install maturin pytest pytest-asynciouv run maturin develop -qpython -m pytest -q -s
Documentation¶
- MkDocs config (
mkdocs.yml) with Material theme and mkdocstrings. - Reference pages pull docstrings directly from the modules.
Roadmap¶
- Async executor in Rust (optional) or hybrid execution with
pyo3-asyncio. - FastAPI integration examples (routers/middleware glue).
- Metrics adapters (Prometheus/OpenTelemetry) using hooks.
- CI: wheel builds (Linux/macos/Windows) and docs publishing.