๐ Technical Design 020 โ External DI Bootstrap Runtime¶
| Field | Value |
|---|---|
| ID | TD-020 |
| Title | Externalize DI Bootstrap Orchestration |
| Status | Completed |
| Owner | Contributors |
| Audience | Contributors, maintainers, DI system users |
| Created | 2025-10-08 |
| Last Updated | 2025-10-10 |
| Reviewers | Contributors |
| Approvers | Contributors |
| Tracking | Phase 2 of DI Roadmap |
| Related | TD-010 (DI Bootstrapping & Initialization), TD-011 (Injection System), TD-013 (Module Scanning) |
๐งญ 1. Summary¶
This design relocates bootstrap orchestration out of DiContainer into a dedicated runtime so that the container remains a pure registry/resolver. Bootstrap is redefined as an idempotent process that composes deterministic registrations, validates dependencies, and finally asks the container to prepare() its eagerly resolvable services. The proposal introduces an external BootstrapRuntime that coordinates planning, dependency validation, and metrics while relying solely on the containerโs public API.
At a glance:
DiContainerbecomes bootstrap-agnostic โ no embeddedBootstrapManager, no self-triggered bootstrap.- New
BootstrapRuntimeencapsulates planning, dependency analysis, and observability, then callscontainer.prepare(). - Migration plan provides an interim delegation layer before removing internal bootstrap hooks.
๐ 2. Context and Problem Statement¶
Phase 2 (TD-010) embedded bootstrap orchestration inside DiContainer. That was expedient but caused several issues:
- Tight coupling:
DiContainerownsBootstrapManager, metrics, and thread coordination, complicating reuse outside โclassicโ flows. - Lifecycle opacity: Higher-level orchestrators (e.g., TD-013 scanning, upcoming configuration binding) must understand internal status transitions to integrate.
- Testing friction: Unit tests often require manual bootstrap manipulation or leakage of internal state to bypass auto-bootstrap.
- Limited extensibility: Adding new bootstrap steps (preflight validation, profiling) requires container changes.
A dedicated runtime allows us to compose bootstrap workstreams (manual, scanning, tests) while keeping the containerโs responsibilities focused.
2.1 Goals¶
- Externalize Orchestration โ Container exposes only registration/prepare APIs; bootstrap coordination lives entirely in the runtime.
- Idempotent Bootstrap โ Re-running bootstrap with the same inputs performs no observable change (deterministic registrations + idempotent container).
- Unified Bootstrap API โ Provide a single entry point (
BootstrapRuntime.bootstrap(container, plan)) used by Application Bootstrap, tests, and tooling. - Observability Preservation โ Retain existing metrics/events (
DiMetrics) while adding runtime-level bootstrap metrics. - Thread-Safe Operation โ Maintain current concurrency guarantees (
RLockdouble-check) in the external runtime.
2.2 Non-Goals (Out of Scope)¶
- Replacing dependency graph analysis algorithms (reuse TD-010 components).
- Introducing new lifecycle scopes (still singleton/transient).
- Reworking annotation or scanning behavior beyond runtime integration points.
- Changing DI metrics schema (only move their invocation site).
2.3 Assumptions and Constraints¶
- TD-010 components (
DependencyAnalyzer, cycle detection utilities,DiMetrics) exist and can be refactored/reused. - Consumers expect
container.bootstrap()and implicit bootstrap on first resolve to continue working until migration completes. - Thread safety and idempotency remain mandatory requirements.
- Scanning (TD-013) and annotations (TD-012) will rely on the new runtime once available.
DiContainercan be updated to expose idempotent registration semantics,prepare(), anditer_registrations()without breaking existing callers.
๐งฉ 3. Design Overview¶
Bootstrap orchestration moves into a standalone runtime that accepts a deterministic โplanโ describing which registrations to apply and what validations to run. The container remains ignorant of bootstrap; it simply receives registration calls and exposes a public prepare() method that eagerly constructs services when requested. Higher-level workflows (ApplicationBootstrap, tests, CLIs) compose plans and invoke the runtime explicitly.
3.1 Key Components¶
| Component | Purpose |
|---|---|
BootstrapRuntime | Coordinates deterministic registrations, validation, and prepare(); emits bootstrap metrics. |
BootstrapPlan | Immutable specification of registration steps and validation/preparation flags. |
RegistrationStep | Protocol for type-safe registration operations (singleton, transient, factory, instance). |
DiContainer | High-level container with idempotency, metrics, locking, and injection features. |
_CoreContainer | Internal low-level registry providing O(1) lookups without business logic. |
DiContainer.prepare() | Public API that eagerly resolves all singleton services, failing fast on first error. |
DependencyGraph | Graph-based dependency tracker using loryth.common.collections.AdjacencyListGraph. |
RegistrationValidator | Helper validating type assignments using reflection. |
ServiceResolver | Helper managing resolution, caching, and injection pipeline. |
DependencyTracker | Helper analyzing constructors and populating dependency graph. |
ContainerMetrics | Container-owned metrics tracking registrations, resolutions, cache hits, and operation timings. |
BootstrapMetrics | Runtime-owned metrics tracking bootstrap executions, scanned modules, validation results, and total durations. |
DependencyAnalyzer | Reused TD-010 component for cycle detection operating directly on DependencyGraph. |
3.2 Architecture¶
flowchart LR
subgraph Orchestrators
A["@application / Tests / CLI"]
end
A --> B[Compose BootstrapPlan]
B --> C[BootstrapRuntime]
subgraph Runtime
C --> E[Execute Registration Steps]
C --> F[DependencyAnalyzer]
C --> G["container.prepare()"]
C --> H[BootstrapMetrics]
end
subgraph "DiContainer (High-Level)"
E --> I[DiContainer.register_*]
I --> V[RegistrationValidator]
I --> T[DependencyTracker]
T --> DG[DependencyGraph]
I --> CC[_CoreContainer]
I --> M[ContainerMetrics]
G --> J["DiContainer.prepare()"]
J --> R[ServiceResolver]
R --> CC
R --> Cache[Singleton Cache]
R --> M
end
F --> DG
H --> L[(Monitoring)]
M --> L
classDef transparent fill:none,stroke:#E6F1FF,color:#E6F1FF;
class A,B,L transparent;
classDef runtime fill:none,stroke:#62C1A1,color:#62C1A1;
class C,E,F,G,H runtime;
classDef container fill:none,stroke:#38AEE6,color:#38AEE6;
class I,J,V,T,DG,CC,R,Cache,M container; 3.3 Interface Impact (Before โ After)¶
| Area | Before | After |
|---|---|---|
DiContainer.__init__ | Instantiates BootstrapManager, status, lock | Private RLock guards all operations; ContainerMetrics instance; no bootstrap state |
DiContainer.bootstrap() | Triggers bootstrap using internal manager | Removed (callers use BootstrapRuntime.bootstrap() directly) |
First resolve() call | Auto-bootstrap via _ensure_bootstrapped | No auto-bootstrap; runtime must be invoked explicitly before resolution |
register_singleton/register_transient/register_factory | Last-wins semantics, no idempotency | Idempotent for identical bindings; conflicting registrations raise DiRegistrationError |
DiContainer.prepare() | N/A | New public API: eagerly resolve all singletons, fail-fast on first error |
DiContainer.metrics | Returns DiMetrics from BootstrapManager | Returns ContainerMetrics instance with current statistics snapshot |
BootstrapRuntime.bootstrap() | N/A | New entry point: execute plan, validate dependencies, call container.prepare() |
BootstrapRuntime.metrics | N/A | Returns BootstrapMetrics instance with cumulative statistics snapshot |
| Thread safety | BootstrapManager owns lock | Container owns private RLock; all register_* and resolve operations acquire lock internally |
3.4 Idempotency Rules¶
To enable safe re-bootstrap scenarios (e.g., after module scanning discovers additional services), all container registration operations must be idempotent. The following rules define when registrations are considered identical versus conflicting:
register_singleton and register_transient¶
Identical Registration: Same (service_type, implementation_type, lifecycle) tuple
- Action: No-op (skip registration silently)
- Rationale: Scanning or repeated bootstrap may discover the same class multiple times
Conflicting Registration: Same service_type but different implementation_type OR different lifecycle
- Action: Raise
DiRegistrationErrorwith clear conflict description - Rationale: Ambiguous intent โ caller must explicitly choose implementation or lifecycle
Examples:
# Identical - second call is no-op
container.register_singleton(IService, ServiceImpl)
container.register_singleton(IService, ServiceImpl) # โ
No-op
# Conflict - different implementation
container.register_singleton(IService, ServiceImplA)
container.register_singleton(IService, ServiceImplB) # โ DiRegistrationError
# Conflict - different lifecycle
container.register_singleton(IService, ServiceImpl)
container.register_transient(IService, ServiceImpl) # โ DiRegistrationError
register_instance¶
Identical Registration: Same (service_type, instance_identity) tuple (compared by id(instance))
- Action: No-op (skip registration silently)
- Rationale: Re-registering the exact same object reference is safe
Conflicting Registration: Same service_type but different instance identity
- Action: Raise
DiRegistrationError - Rationale: Multiple distinct instances for same service type is ambiguous
Examples:
instance = ServiceImpl()
# Identical - same instance reference
container.register_instance(IService, instance)
container.register_instance(IService, instance) # โ
No-op
# Conflict - different instance
instance2 = ServiceImpl()
container.register_instance(IService, instance)
container.register_instance(IService, instance2) # โ DiRegistrationError
register_factory¶
Identical Registration: Same (service_type, factory_identity) tuple (compared by id(factory))
- Action: No-op (skip registration silently)
- Rationale: Re-registering the exact same factory function reference is safe
- Limitation: Lambda functions are never identical (each lambda creates a new function object)
Conflicting Registration: Same service_type but different factory identity
- Action: Raise
DiRegistrationError - Rationale: Multiple factory implementations for same service type is ambiguous
Examples:
def create_service(container):
return ServiceImpl()
# Identical - same function reference
container.register_factory(IService, create_service)
container.register_factory(IService, create_service) # โ
No-op
# Conflict - different function (even if semantically equivalent)
def create_service_alt(container):
return ServiceImpl()
container.register_factory(IService, create_service)
container.register_factory(IService, create_service_alt) # โ DiRegistrationError
# Always conflicts - lambdas are never identical by reference
container.register_factory(IService, lambda c: ServiceImpl())
container.register_factory(IService, lambda c: ServiceImpl()) # โ DiRegistrationError
Workaround for Lambda Idempotency: Use named functions or store lambda references explicitly:
# Option 1: Named function (preferred)
def create_service(c): return ServiceImpl()
container.register_factory(IService, create_service)
# Option 2: Store lambda reference
factory = lambda c: ServiceImpl()
container.register_factory(IService, factory)
container.register_factory(IService, factory) # โ
No-op (same reference)
Cross-Type Registration Conflicts¶
Rule: A service_type can only have one active registration regardless of registration method.
Rationale: Ambiguous resolution behavior - the container cannot determine which registration to use for resolve(service_type).
Conflicting Scenarios:
- Singleton + Transient of same type (covered above)
- Singleton + Factory of same type
- Transient + Factory of same type
- Singleton + Instance of same type
- Transient + Instance of same type
- Factory + Instance of same type
- Any combination of different registration methods for the same service type
Action: Raise DiRegistrationError when attempting to register a service_type that already has a registration via a different method.
Examples:
# Conflict - singleton then factory
container.register_singleton(IService, ServiceImpl)
container.register_factory(IService, lambda c: ServiceImpl())
# โ DiRegistrationError: Cannot register factory for IService - already registered as singleton
# Conflict - transient then instance
container.register_transient(ILogger, ConsoleLogger)
logger_instance = ConsoleLogger()
container.register_instance(ILogger, logger_instance)
# โ DiRegistrationError: Cannot register instance for ILogger - already registered as transient
# Conflict - factory then singleton
container.register_factory(IDatabase, lambda c: DatabaseConnection())
container.register_singleton(IDatabase, DatabaseConnection)
# โ DiRegistrationError: Cannot register singleton for IDatabase - already registered as factory
Replacement Workflow: To change registration type, explicitly unregister first (future feature):
# Manual replacement (requires unregister feature - out of TD-020 scope)
container.unregister(IService) # Remove existing registration
container.register_factory(IService, lambda c: NewServiceImpl()) # New registration allowed
3.5 Metrics Design¶
Metrics are split between container-owned (ContainerMetrics) tracking operational statistics and runtime-owned (BootstrapMetrics) tracking bootstrap execution. Both expose read-only snapshot properties for observability.
ContainerMetrics¶
Container-owned metrics track registration, resolution, and cache behavior. The container updates metrics synchronously during operations and exposes a snapshot property for monitoring.
Tracked Metrics:
- Registrations: Total count and breakdown by lifecycle (
singleton/transient/factory/instance) - Resolutions: Total attempts, successes, failures (by exception type)
- Cache Performance: Cache hits, cache misses, current singleton count
- Instantiations: Total instances created (transient + singleton on first resolve)
- Timings: Per-operation duration tracking (registration, resolution, cache access)
API:
class ContainerMetrics:
"""Container-owned operational metrics."""
@property
def snapshot(self) -> dict[str, Any]:
"""Return current metrics snapshot for monitoring."""
return {
"registrations": {
"total": self._registration_count,
"by_lifecycle": {
"singleton": self._singleton_count,
"transient": self._transient_count,
"factory": self._factory_count,
"instance": self._instance_count,
},
},
"resolutions": {
"total": self._resolution_count,
"successes": self._resolution_success_count,
"failures": self._resolution_failure_count,
"average_duration_ms": self._avg_resolution_duration_ms,
},
"cache": {
"hits": self._cache_hit_count,
"misses": self._cache_miss_count,
"hit_rate": self._cache_hit_rate,
"current_singletons": self._current_singleton_count,
},
"instantiations": {
"total": self._instantiation_count,
"average_duration_ms": self._avg_instantiation_duration_ms,
},
}
# Usage
container = DiContainer()
container.register_singleton(IService, Service)
print(container.metrics.snapshot) # Current statistics
BootstrapMetrics¶
Runtime-owned metrics track bootstrap execution across all containers bootstrapped by a runtime instance. Metrics accumulate across multiple bootstrap() calls and expose a cumulative snapshot.
Tracked Metrics:
- Bootstrap Executions: Total attempts, successes, failures
- Registration Steps: Total steps executed, steps per bootstrap
- Scanned Modules: Total modules/classes discovered during scanning (if applicable)
- Validation Results: Cycles detected, dependency errors found
- Timings: Total bootstrap duration, average duration, breakdown by phase (registration/validation/preparation)
API:
class BootstrapMetrics:
"""Runtime-owned bootstrap execution metrics."""
@property
def snapshot(self) -> dict[str, Any]:
"""Return cumulative metrics snapshot for monitoring."""
return {
"bootstraps": {
"total": self._bootstrap_count,
"successes": self._success_count,
"failures": self._failure_count,
"success_rate": self._success_rate,
"average_duration_ms": self._avg_bootstrap_duration_ms,
},
"registrations": {
"total_steps_executed": self._total_registration_steps,
"average_steps_per_bootstrap": self._avg_steps_per_bootstrap,
},
"scanning": {
"modules_scanned": self._modules_scanned_count,
"classes_discovered": self._classes_discovered_count,
},
"validation": {
"cycles_detected": self._cycles_detected_count,
"dependency_errors": self._dependency_error_count,
},
"timings": {
"total_duration_ms": self._total_bootstrap_duration_ms,
"phase_breakdown_ms": {
"registration": self._registration_phase_duration_ms,
"validation": self._validation_phase_duration_ms,
"preparation": self._preparation_phase_duration_ms,
},
},
}
# Usage
runtime = BootstrapRuntime()
runtime.bootstrap(container, plan)
print(runtime.metrics.snapshot) # Cumulative statistics across all bootstraps
Metrics Access Patterns¶
# Container metrics - operational snapshot
container = DiContainer()
container.register_singleton(IService, Service)
container.resolve(IService)
stats = container.metrics.snapshot
print(f"Cache hit rate: {stats['cache']['hit_rate']:.2%}")
# Bootstrap metrics - cumulative execution tracking
runtime = BootstrapRuntime()
runtime.bootstrap(container1, plan)
runtime.bootstrap(container2, plan)
stats = runtime.metrics.snapshot
print(f"Total bootstraps: {stats['bootstraps']['total']}")
print(f"Success rate: {stats['bootstraps']['success_rate']:.2%}")
3.6 Removal/Deprecation Policy¶
This design completely removes embedded bootstrap logic from DiContainer. The rollout is structured to ensure clean migration:
- Phases A-C: Build runtime, refactor container internals, and extract helper components while maintaining compatibility.
- Phase D: Remove all legacy bootstrap APIs (
DiContainer.bootstrap(), auto-bootstrap,_bootstrap_manager). - Phase E: Migrate all integration points (ApplicationBootstrap, annotation processing) to use
BootstrapRuntimedirectly. - Phase F: Update documentation (TD-010) to reflect external bootstrap ownership; remove all deprecated API references.
๐ 4. Implementation Details¶
4.1 Registration Steps¶
Registration steps are type-safe wrappers around container registration operations. Each subclass corresponds to a DiContainer.register_* method, enabling plan-based registration with compile-time type safety.
from typing import Protocol, Callable, Any
from dataclasses import dataclass
class RegistrationStep(Protocol):
"""Protocol for type-safe container registration operations."""
def apply(self, container: "DiContainer") -> None:
"""Execute this registration step on the provided container."""
...
@dataclass(frozen=True)
class SingletonRegistrationStep:
"""Register a singleton service."""
service_type: type
implementation_type: type
def apply(self, container: "DiContainer") -> None:
container.register_singleton(self.service_type, self.implementation_type)
@dataclass(frozen=True)
class TransientRegistrationStep:
"""Register a transient service."""
service_type: type
implementation_type: type
def apply(self, container: "DiContainer") -> None:
container.register_transient(self.service_type, self.implementation_type)
@dataclass(frozen=True)
class FactoryRegistrationStep:
"""Register a factory function."""
service_type: type
factory: Callable[["DiContainer"], Any]
def apply(self, container: "DiContainer") -> None:
container.register_factory(self.service_type, self.factory)
@dataclass(frozen=True)
class InstanceRegistrationStep:
"""Register a pre-created instance."""
service_type: type
instance: Any
def apply(self, container: "DiContainer") -> None:
container.register_instance(self.service_type, self.instance)
Usage Examples:
# Type-safe plan construction
plan = BootstrapPlan(
registrations=(
SingletonRegistrationStep(IUserService, UserService),
TransientRegistrationStep(ILogger, ConsoleLogger),
FactoryRegistrationStep(IDatabase, lambda c: create_database(c.resolve(IConfig))),
InstanceRegistrationStep(IConfig, load_config()),
),
validate_dependencies=True,
eager_prepare=True,
)
4.2 BootstrapPlan & Runtime¶
The bootstrap plan specifies which registration steps to execute and which validation/preparation phases to run. The runtime orchestrates execution, tracking metrics and ensuring thread safety through the container's lock.
@dataclass(frozen=True)
class BootstrapPlan:
"""Immutable bootstrap execution plan."""
registrations: tuple[RegistrationStep, ...] = ()
validate_dependencies: bool = True
eager_prepare: bool = True
class DependencyAnalyzer:
"""Reuses TD-010 cycle detection operating directly on DependencyGraph."""
def validate(self, dependency_graph: DependencyGraph) -> None:
"""Detect cycles using graph structure via topological sort.
Raises:
BootstrapError: If a cycle is detected in the dependency graph
"""
from loryth.common.collections import topological_sort
try:
# topological_sort raises ValueError if graph contains cycles
topological_sort(dependency_graph._graph)
except ValueError as e:
raise BootstrapError(f"Circular dependency detected: {e}") from e
class BootstrapRuntime:
"""External bootstrap orchestrator coordinating registration, validation, and preparation."""
def __init__(
self,
*,
dependency_analyzer: DependencyAnalyzer | None = None,
) -> None:
self._dependency_analyzer = dependency_analyzer or DependencyAnalyzer()
self._metrics = BootstrapMetrics()
@property
def metrics(self) -> BootstrapMetrics:
"""Return bootstrap metrics snapshot."""
return self._metrics
def bootstrap(
self,
container: DiContainer,
plan: BootstrapPlan,
) -> None:
"""Execute bootstrap plan with metrics tracking."""
# Container handles thread safety via its own lock
# Runtime coordinates phases without owning locks
start = time.perf_counter()
try:
# Phase 1: Execute registration steps
registration_start = time.perf_counter()
for step in plan.registrations:
step.apply(container) # Idempotent via container rules
registration_duration = time.perf_counter() - registration_start
# Phase 2: Validate dependencies (if enabled)
validation_duration = 0.0
if plan.validate_dependencies:
validation_start = time.perf_counter()
self._dependency_analyzer.validate(container._dependency_graph)
validation_duration = time.perf_counter() - validation_start
# Phase 3: Eager prepare singletons (if enabled)
preparation_duration = 0.0
if plan.eager_prepare:
preparation_start = time.perf_counter()
container.prepare() # Fail-fast on first error
preparation_duration = time.perf_counter() - preparation_start
# Record success
total_duration = time.perf_counter() - start
self._metrics._record_success(
step_count=len(plan.registrations),
total_duration=total_duration,
registration_duration=registration_duration,
validation_duration=validation_duration,
preparation_duration=preparation_duration,
)
except Exception as exc:
# Record failure and propagate
total_duration = time.perf_counter() - start
self._metrics._record_failure(exc, total_duration)
raise
Key Behaviors:
- Thread Safety: Container owns the lock and guards all
register_*andresolveoperations internally. Runtime does not manage locks. - Idempotency: Registration steps can be re-executed safely thanks to container-level idempotency rules (section 3.4).
- Fail-Fast:
container.prepare()raises on first singleton resolution failure (per Q1 requirement). - Metrics: Runtime tracks cumulative bootstrap statistics across all
bootstrap()calls.
4.3 Container Internal Design¶
The container is refactored into a layered architecture with an internal core registry and external helper components.
Container Items (Discriminated Union)¶
Container items use a Protocol-based discriminated union with embedded business logic:
from typing import Protocol, Any, Callable
from dataclasses import dataclass
from enum import Enum, auto
class RegistrationKind(Enum):
"""Discriminator for registration types."""
TYPE_BASED_SINGLETON = auto()
TYPE_BASED_TRANSIENT = auto()
FACTORY = auto()
INSTANCE = auto()
@dataclass(frozen=True)
class RegistrationKey:
"""Hashable key for O(1) idempotency checks."""
service_type: type
kind: RegistrationKind
implementation_type_id: int | None = None # id() for TYPE_BASED_*
factory_id: int | None = None # id() for FACTORY
instance_id: int | None = None # id() for INSTANCE
class DiContainerItem(Protocol):
"""Protocol for container registrations."""
@property
def service_type(self) -> type: ...
@property
def key(self) -> RegistrationKey: ...
@property
def lifecycle(self) -> ServiceLifecycle: ...
def create_instance(self, container: "DiContainer") -> Any: ...
def conflicts_with(self, other: "DiContainerItem") -> bool: ...
@dataclass(frozen=True)
class TypeBasedRegistration:
"""Registration for singleton/transient via type."""
_service_type: type
_implementation_type: type
_lifecycle: ServiceLifecycle
_provider: Callable[["DiContainer"], Any]
@property
def service_type(self) -> type:
return self._service_type
@property
def key(self) -> RegistrationKey:
kind = (
RegistrationKind.TYPE_BASED_SINGLETON
if self._lifecycle == ServiceLifecycle.SINGLETON
else RegistrationKind.TYPE_BASED_TRANSIENT
)
return RegistrationKey(
service_type=self._service_type,
kind=kind,
implementation_type_id=id(self._implementation_type),
)
@property
def lifecycle(self) -> ServiceLifecycle:
return self._lifecycle
def create_instance(self, container: "DiContainer") -> Any:
return self._provider(container)
def conflicts_with(self, other: DiContainerItem) -> bool:
return self.service_type == other.service_type and self.key != other.key
@dataclass(frozen=True)
class FactoryRegistration:
"""Registration for factory-based service creation."""
_service_type: type
_factory: Callable[["DiContainer"], Any]
_lifecycle: ServiceLifecycle # Factories are always transient
@property
def service_type(self) -> type:
return self._service_type
@property
def key(self) -> RegistrationKey:
return RegistrationKey(
service_type=self._service_type,
kind=RegistrationKind.FACTORY,
factory_id=id(self._factory),
)
@property
def lifecycle(self) -> ServiceLifecycle:
return self._lifecycle
def create_instance(self, container: "DiContainer") -> Any:
return self._factory(container)
def conflicts_with(self, other: DiContainerItem) -> bool:
return self.service_type == other.service_type and self.key != other.key
@dataclass(frozen=True)
class InstanceRegistration:
"""Registration for pre-created instance."""
_service_type: type
_instance: Any
@property
def service_type(self) -> type:
return self._service_type
@property
def key(self) -> RegistrationKey:
return RegistrationKey(
service_type=self._service_type,
kind=RegistrationKind.INSTANCE,
instance_id=id(self._instance),
)
@property
def lifecycle(self) -> ServiceLifecycle:
return ServiceLifecycle.SINGLETON # Instances are always singleton
def create_instance(self, container: "DiContainer") -> Any:
return self._instance
def conflicts_with(self, other: DiContainerItem) -> bool:
return self.service_type == other.service_type and self.key != other.key
_CoreContainer (Internal Registry)¶
Low-level registry providing O(1) lookups without business logic:
class _CoreContainer:
"""Internal registry with optimized data structures."""
def __init__(self):
# Primary resolution index: service_type โ current registration
self._current: dict[type, DiContainerItem] = {}
# Idempotency index: RegistrationKey โ DiContainerItem
self._registered_keys: dict[RegistrationKey, DiContainerItem] = {}
def add(self, item: DiContainerItem) -> None:
"""Add item without validation (caller ensures correctness)."""
self._current[item.service_type] = item
self._registered_keys[item.key] = item
def get(self, service_type: type) -> DiContainerItem | None:
"""Get registration for service type (O(1))."""
return self._current.get(service_type)
def has_key(self, key: RegistrationKey) -> bool:
"""Check if key is registered (O(1))."""
return key in self._registered_keys
def items(self) -> Iterator[DiContainerItem]:
"""Iterate all current registrations."""
return iter(self._current.values())
DependencyGraph (Using collections.Graph)¶
from loryth.common.collections import AdjacencyListGraph
class DependencyGraph:
"""Graph-based dependency tracker for cycle detection."""
def __init__(self):
self._graph: AdjacencyListGraph[type, None] = AdjacencyListGraph()
def add_service(self, service_type: type) -> None:
"""Add service node to graph."""
if not self._graph.has_node(service_type):
self._graph.add_node(service_type)
def add_dependencies(self, service_type: type, depends_on: set[type]) -> None:
"""Record dependencies as graph edges."""
self.add_service(service_type)
for dependency in depends_on:
self.add_service(dependency)
if not self._graph.has_edge(service_type, dependency):
self._graph.add_edge(service_type, dependency, None)
def get_dependencies(self, service_type: type) -> set[type]:
"""Get direct dependencies (O(1) lookup + O(d) iteration)."""
if not self._graph.has_node(service_type):
return set()
return {edge.target for edge in self._graph.find_edges_by_source(service_type)}
def iter_services(self) -> Iterator[type]:
"""Iterate all service nodes."""
return self._graph.nodes
def clear(self) -> None:
"""Clear all nodes and edges."""
self._graph = AdjacencyListGraph()
Helper Components¶
RegistrationValidator: Validates type assignments using reflection:
class RegistrationValidator:
"""Validates service registrations using reflection."""
def __init__(self, reflection_context):
self._reflection = reflection_context
def validate_registration(self, service_type: type, implementation_type: type) -> None:
"""Validate that implementation is assignable to service type.
Raises:
DiRegistrationError: If types are incompatible
"""
impl_info = self._reflection.of_type(implementation_type)
service_info = self._reflection.of_type(service_type)
if not self._is_assignable(impl_info, service_info):
raise DiRegistrationError(
f"{impl_info.qualname} is not assignable to {service_info.qualname}"
)
def _is_assignable(self, impl_info, service_info) -> bool:
"""Check assignability using reflection (handles protocols, ABC, concrete types)."""
# Implementation details from current container._is_assignable_registration
...
ServiceResolver: Manages resolution, caching, and injection pipeline:
class ServiceResolver:
"""Resolves services with caching and injection support."""
def __init__(
self,
container: "DiContainer",
injection_manager: InjectionManager,
cache: dict[type, Any],
metrics: ContainerMetrics,
):
self._container = container
self._injection_manager = injection_manager
self._cache = cache
self._metrics = metrics
def resolve(self, service_type: type) -> Any:
"""Resolve service with cache check and injection.
Raises:
DependencyNotFoundError: If service not registered
DiResolutionError: If resolution fails
"""
start_time = time.perf_counter()
try:
# Check cache
if service_type in self._cache:
self._metrics.record_cache_hit(service_type, time.perf_counter() - start_time)
return self._cache[service_type]
# Find registration
item = self._container._core.get(service_type)
if item is None:
raise DependencyNotFoundError(service_type)
# Create and inject
instance = item.create_instance(self._container)
injected = self._injection_manager._apply_all_injectors(instance)
# Cache singletons
if item.lifecycle == ServiceLifecycle.SINGLETON:
self._cache[service_type] = injected
self._metrics.record_resolution_success(service_type, time.perf_counter() - start_time)
return injected
except Exception as e:
self._metrics.record_resolution_failure(service_type, e, time.perf_counter() - start_time)
raise
DependencyTracker: Analyzes constructors and populates dependency graph:
class DependencyTracker:
"""Tracks service dependencies for validation."""
def __init__(
self,
constructor_injector: ConstructorInjector,
dependency_graph: DependencyGraph,
):
self._constructor_injector = constructor_injector
self._dependency_graph = dependency_graph
def track_dependencies(self, service_type: type, implementation_type: type) -> None:
"""Analyze constructor and add dependencies to graph."""
# Analyze constructor parameters
metadata = self._constructor_injector.analyze(implementation_type)
# Extract dependency types (ignoring optional flag for graph purposes)
dependencies = {dep_type for dep_type, _ in metadata}
# Update graph
self._dependency_graph.add_dependencies(service_type, dependencies)
DiContainer (High-Level with Helpers)¶
class DiContainer:
"""High-level container with idempotency, metrics, locking, and injection."""
def __init__(self):
# Core state
self._core = _CoreContainer()
self._cache: dict[type, Any] = {}
self._lock = threading.RLock() # Private lock for thread safety
self._dependency_graph = DependencyGraph()
# Metrics
self._metrics = ContainerMetrics()
# Helper components
self._validator = RegistrationValidator(reflection)
self._resolver = ServiceResolver(
container=self,
injection_manager=InjectionManager(self),
cache=self._cache,
metrics=self._metrics,
)
self._dependency_tracker = DependencyTracker(
constructor_injector=ConstructorInjector(self),
dependency_graph=self._dependency_graph,
)
def register_singleton(self, service_type, impl_type):
"""Register singleton with idempotency and validation."""
with self._lock:
self._validator.validate_registration(service_type, impl_type)
item = TypeBasedRegistration(
_service_type=service_type,
_implementation_type=impl_type,
_lifecycle=ServiceLifecycle.SINGLETON,
_provider=self._create_singleton_provider(impl_type),
)
self._register_item(item)
self._dependency_tracker.track_dependencies(service_type, impl_type)
def _register_item(self, item: DiContainerItem):
"""Common registration logic with O(1) idempotency checks."""
# Check idempotency
if self._core.has_key(item.key):
return # Identical - no-op
# Check conflicts
existing = self._core.get(item.service_type)
if existing and existing.conflicts_with(item):
raise DiRegistrationError(
f"Conflicting registration for {item.service_type.__name__}: "
f"existing={existing.key.kind.name}, new={item.key.kind.name}"
)
# Register
self._core.add(item)
self._metrics.record_registration(item.lifecycle)
def resolve(self, service_type):
"""Resolve service via resolver helper."""
with self._lock:
return self._resolver.resolve(service_type)
def prepare(self):
"""Eagerly resolve all singletons (fail-fast)."""
with self._lock:
for item in self._core.items():
if item.lifecycle == ServiceLifecycle.SINGLETON:
self.resolve(item.service_type) # Raises on first error
@property
def metrics(self) -> ContainerMetrics:
"""Return container metrics snapshot."""
return self._metrics
Key Improvements:
- โ
O(1) lookups via
_CoreContainerindices - โ Separation of concerns (core registry vs business logic)
- โ Graph-based dependency tracking using existing collections
- โ Helper components for validation, resolution, dependency analysis
- โ
No
resolve_all()- one service type = one registration
4.4 Integration Touchpoints¶
ApplicationBootstrap (TD-013): Scanning pipeline builds BootstrapPlan, then invokes runtime:
class ApplicationBootstrap:
def bootstrap_application(self, app_class: type) -> DiContainer:
container = DiContainer()
plan = self._build_plan_from_scanning(app_class)
runtime = BootstrapRuntime()
runtime.bootstrap(container, plan)
return container
Tests: Directly construct plans and invoke runtime:
def test_service_resolution():
container = DiContainer()
plan = BootstrapPlan(
registrations=(
SingletonRegistrationStep(IService, Service),
),
validate_dependencies=True,
eager_prepare=True,
)
runtime = BootstrapRuntime()
runtime.bootstrap(container, plan)
service = container.resolve(IService)
assert isinstance(service, Service)
Annotation Processor (TD-012): Processor builds registration steps from annotated classes:
class AnnotationProcessor:
def process(self, classes: list[type]) -> BootstrapPlan:
steps = []
for cls in classes:
annotation = find_service_annotation(cls)
if annotation.lifecycle == ServiceLifecycle.SINGLETON:
steps.append(SingletonRegistrationStep(cls, cls))
elif annotation.lifecycle == ServiceLifecycle.TRANSIENT:
steps.append(TransientRegistrationStep(cls, cls))
return BootstrapPlan(registrations=tuple(steps))
โ๏ธ 5. Semantics¶
Thread Safety¶
- Container Lock:
DiContainerowns a privateRLock(internal implementation detail) that guards all registration and resolution operations - Automatic Acquisition: All
register_*andresolvemethods acquire the lock internally - No Runtime Locks:
BootstrapRuntimedoes not manage locks; thread safety is guaranteed by container operations - Reentrancy: Lock is reentrant (
RLock) to support nested resolution (singletons resolving dependencies) - Encapsulation: Lock is not exposed via public API; callers do not interact with locking mechanisms directly
Idempotency¶
- Registration Rules: See section 3.4 for complete idempotency semantics per registration type
- Identical Operations: No-op behavior for exact duplicates (same types/instances/factories by reference)
- Conflicting Operations: Raise
DiRegistrationErrorfor ambiguous registrations - Re-bootstrap Safety: Entire bootstrap process can be re-executed with same plan (deterministic + idempotent)
Preparation¶
- Eager Resolution:
container.prepare()resolves all singleton registrations immediately - Fail-Fast: Raises on first resolution error; does not aggregate failures
- Cache Population: Successfully resolved singletons are cached; subsequent
resolve()calls return cached instances - No Side Effects: Runtime logic is side-effect free; only container operations mutate state
Error Handling¶
- Validation Failures:
DependencyAnalyzer.validate()raisesBootstrapErroron cycles or missing dependencies - Preparation Failures:
container.prepare()raises resolution errors directly (wrapped inDiResolutionError) - Metrics Recording: Runtime records failures before propagating exceptions
- Partial State: Container may have partial registrations/cache on failure; callers should discard failed containers
๐งช 6. Examples¶
6.1 Basic Bootstrap Flow¶
from loryth.common.di.bootstrap import (
BootstrapPlan,
BootstrapRuntime,
SingletonRegistrationStep,
TransientRegistrationStep,
)
from loryth.common.di.container import Container
# Create container and plan
container = Container()
plan = BootstrapPlan(
registrations=(
SingletonRegistrationStep(ICacheWarmup, CacheWarmup),
TransientRegistrationStep(ILogger, ConsoleLogger),
),
validate_dependencies=True,
eager_prepare=True,
)
# Execute bootstrap
runtime = BootstrapRuntime()
runtime.bootstrap(container, plan)
# Resolve services
service = container.resolve(ICacheWarmup)
logger = container.resolve(ILogger)
# Check metrics
print(container.metrics.snapshot)
print(runtime.metrics.snapshot)
6.2 Manual Registration with Bootstrap Validation¶
# Register services directly on container
container = DiContainer()
container.register_singleton(IUserService, UserService)
container.register_transient(IEmailService, EmailService)
container.register_factory(IDatabase, lambda c: create_db(c.resolve(IConfig)))
# Bootstrap with validation only (no registration steps)
plan = BootstrapPlan(
registrations=(), # Empty - services already registered
validate_dependencies=True,
eager_prepare=True,
)
runtime = BootstrapRuntime()
runtime.bootstrap(container, plan)
6.3 Testing with Validation Disabled¶
def test_service_with_incomplete_dependencies():
"""Test service resolution without validating full dependency graph."""
container = DiContainer()
plan = BootstrapPlan(
registrations=(
SingletonRegistrationStep(IService, Service),
# Service depends on ILogger, but we're not registering it
),
validate_dependencies=False, # Skip cycle detection
eager_prepare=False, # Don't resolve eagerly
)
runtime = BootstrapRuntime()
runtime.bootstrap(container, plan)
# Container is ready but will fail on resolve if dependencies missing
# This is useful for unit tests that mock dependencies
6.4 Integration with ApplicationBootstrap (TD-013)¶
from loryth.common.di.bootstrap import BootstrapPlan, BootstrapRuntime
class ApplicationBootstrap:
def __init__(self, scanner: DiPackageScanner):
self._scanner = scanner
self._runtime = BootstrapRuntime()
def run(self, app_cls: type) -> DiContainer:
# Scan for annotated services
scanned_classes = self._scanner.scan(ScanningConfiguration(...))
# Build registration plan from scanned classes
steps = []
for cls in scanned_classes:
annotation = find_service_annotation(cls)
if annotation.lifecycle == ServiceLifecycle.SINGLETON:
steps.append(SingletonRegistrationStep(cls, cls))
elif annotation.lifecycle == ServiceLifecycle.TRANSIENT:
steps.append(TransientRegistrationStep(cls, cls))
# Create container and bootstrap
container = DiContainer()
plan = BootstrapPlan(registrations=tuple(steps))
self._runtime.bootstrap(container, plan)
return container
6.5 Factory Registration with Instance Injection¶
# Pre-load configuration
config = load_config_from_file("app.yaml")
# Build plan with mixed registration types
plan = BootstrapPlan(
registrations=(
InstanceRegistrationStep(IConfig, config), # Pre-created instance
FactoryRegistrationStep(
IDatabase,
lambda c: DatabaseConnection(c.resolve(IConfig).db_url),
),
SingletonRegistrationStep(IUserService, UserService),
),
)
runtime = BootstrapRuntime()
runtime.bootstrap(container, plan)
# All services resolved with config injected
user_service = container.resolve(IUserService)
6.6 Idempotent Re-Bootstrap¶
# Initial bootstrap
container = DiContainer()
plan1 = BootstrapPlan(
registrations=(
SingletonRegistrationStep(IServiceA, ServiceA),
),
)
runtime = BootstrapRuntime()
runtime.bootstrap(container, plan1)
# Later: discover more services (e.g., from plugin scanning)
plan2 = BootstrapPlan(
registrations=(
SingletonRegistrationStep(IServiceA, ServiceA), # Duplicate - no-op
SingletonRegistrationStep(IServiceB, ServiceB), # New service
),
)
runtime.bootstrap(container, plan2) # Safe - idempotent
# Check metrics
stats = runtime.metrics.snapshot
print(f"Total bootstraps: {stats['bootstraps']['total']}") # 2
print(f"Total registration steps: {stats['registrations']['total_steps_executed']}") # 3 (2 from plan2, 1 skipped)
๐งช 7. Test Matrix¶
tests/unit/di/bootstrap/
โโโ test_bootstrap_runtime.py # orchestration, metrics, phase coordination
โโโ test_bootstrap_plan.py # immutable plan construction
โโโ test_container_prepare.py # prepare() eager hydration with fail-fast behavior
tests/unit/di/container/
โโโ test_core_container.py # _CoreContainer O(1) lookups and idempotency
โโโ test_dependency_graph.py # graph-based dependency tracking
โโโ test_container_items.py # discriminated union item types and conflict detection
โโโ test_registration_helpers.py # RegistrationValidator, ServiceResolver, DependencyTracker
tests/integration/di/bootstrap/
โโโ test_application_bootstrap.py # ApplicationBootstrap end-to-end flow
โโโ test_repeat_bootstrap.py # idempotent re-bootstrap
โโโ test_bootstrap_failures.py # validation and prepare failure propagation
7.1 Unit Coverage¶
- Deterministic Registrations: Multiple invocations of identical
RegistrationSteps do not mutate container state. - Container Thread Safety: Concurrent registrations/resolutions properly serialize via container's internal lock.
- Dependency Validation: Cycles detected via
DependencyAnalyzer.validate(DependencyGraph). - Prepare Behavior:
container.prepare()hydrates all singleton registrations and fails fast on first error. - Bootstrap Metrics: Success/failure paths emit expected bootstrap events with phase-level timing breakdown.
- Idempotency Checks: O(1) duplicate detection via
RegistrationKeyand conflict detection viaconflicts_with(). - Graph Operations:
DependencyGraphcorrectly adds nodes/edges usingAdjacencyListGraph.
7.2 Integration Coverage¶
- Application Bootstrap Flow:
ApplicationBootstrap+ runtime produce a configured container and invokeprepare(). - Repeated Bootstrap Idempotency: Re-running bootstrap after scanning does not duplicate registrations or rehydrate services unnecessarily.
- Failure Propagation: Dependency validation failures and singleton construction errors propagate through runtime with actionable errors.
- Cross-Type Conflicts: Registering same service type with different methods (singleton โ factory) raises errors.
- Helper Component Integration: Validator, resolver, and tracker work together in registration/resolution flows.
Testing notes:
- Unit tests use lightweight fake registrations and mock helpers for isolation.
- Integration tests use real scanning/annotation flows to reflect production usage.
- No tests for removed features (
resolve_all,bootstrap_container()helper, lock pool).
โ 8. Acceptance Criteria¶
-
DiContainerno longer instantiates or ownsBootstrapManager; uses_CoreContainerand helper components. -
DiContainerregistrations are idempotent with O(1) checks viaRegistrationKey. -
DiContainer.prepare()publicly exposed and fails fast on first singleton error. -
DependencyGraphusesloryth.common.collections.AdjacencyListGraphfor dependency tracking. -
BootstrapRuntime+BootstrapPlanimplemented with metrics andDependencyAnalyzerintegration. -
DependencyAnalyzer.validate()operates directly onDependencyGraph(not dict conversion). - Legacy APIs (
DiContainer.bootstrap(), implicit auto-bootstrap) removed entirely. - Application bootstrap (TD-013) and annotation processing use
BootstrapRuntimedirectly. - All scenarios listed in the test matrix are covered by automated tests.
- Comprehensive tests cover explicit bootstrap, cross-type conflicts, idempotency, and thread safety.
๐ 9. Rollout and Migration¶
-
Phase A โ Runtime Foundations
- Implement
BootstrapRuntime,BootstrapPlan, and registration step subclasses. - Implement
BootstrapMetricswith concrete state tracking (no sink pattern). - Implement
DependencyAnalyzeroperating directly onDependencyGraph. - Add unit tests covering deterministic plans, dependency validation, and metrics emission.
- Implement
-
Phase B โ Container Internal Refactoring
- Implement
_CoreContainerwith dual-index structure (_current,_registered_keys). - Implement
DiContainerItemprotocol with discriminated union (TypeBasedRegistration, etc.). - Implement
RegistrationKeywithid()-based identity fields. - Implement
DependencyGraphusingloryth.common.collections.AdjacencyListGraph. - Add unit tests for
_CoreContainer,DependencyGraph, and container items.
- Implement
-
Phase C โ Helper Component Extraction
- Extract
RegistrationValidatorfrom container validation logic. - Extract
ServiceResolverfrom resolution logic. - Extract
DependencyTrackerfrom dependency analysis logic. - Implement
ContainerMetricswith concrete state (registrations, resolutions, cache hits). - Refactor
DiContainerto use helpers and_CoreContainer. - Add unit tests for each helper component.
- Extract
-
Phase D โ Container API Updates
- Make registration APIs idempotent using
RegistrationKeychecks. - Implement public
prepare()with fail-fast behavior. - Expose
metricsproperty returningContainerMetricssnapshot. - Delete
_bootstrap_manager,_bootstrap_lock,_status,_ensure_bootstrapped. - Delete
resolve_all()method. - Add unit tests for idempotent registrations and cross-type conflict detection.
- Make registration APIs idempotent using
-
Phase E โ Integration Adoption
- Update ApplicationBootstrap to use
BootstrapRuntime.bootstrap()directly. - Update annotation processing to build
BootstrapPlanwith registration steps. - Update all integration tests to call runtime directly (no helper).
- Add integration tests for idempotent re-bootstrap and failure propagation.
- Update ApplicationBootstrap to use
-
Phase F โ Documentation and Cleanup
- Update TD-010 to reference external bootstrap runtime.
- Update user guides with new bootstrap patterns.
- Remove all deprecated API references.
- Final sweep across all DI documentation.
โ ๏ธ 10. Risks and Mitigations¶
| Risk | Mitigation |
|---|---|
| Divergent bootstrap usage (some callers forget to bootstrap) | Clear documentation; runtime is explicit entry point; tests demonstrate correct usage patterns. |
| Metrics regression | Maintain compatibility tests; ContainerMetrics and BootstrapMetrics track same events as before. |
| Thread-safety regressions | Stress tests with concurrent registrations/resolutions; container's internal RLock ensures thread safety. |
| Performance degradation from helper indirection | Profile registration/resolution paths; O(1) lookups via _CoreContainer should maintain or improve performance. |
| Graph library compatibility issues | Unit tests for DependencyGraph using AdjacencyListGraph; validate cycle detection with known edge cases. |
๐ 11. Progress¶
Legend: โณ Pending ยท ๐ง In Progress ยท โ Completed
-
Phase A โ Runtime Foundations
- โ
BootstrapRuntimeorchestrates deterministic registrations. - โ
BootstrapPlanwith registration step protocol. - โ
BootstrapMetricswith concrete state tracking. - โ
DependencyAnalyzeroperates onDependencyGraphdirectly. - โ Unit tests cover plans, validation, and metrics.
- โ
-
Phase B โ Container Internal Refactoring
- โ
_CoreContainerwith O(1) dual-index structure. - โ
DiContainerItemprotocol with discriminated union. - โ
RegistrationKeywithid()-based identity. - โ
DependencyGraphusingAdjacencyListGraph. - โ Unit tests for core container and graph.
- โ
-
Phase C โ Helper Component Extraction
- โ
RegistrationValidatorextracted from container. - โ
ServiceResolverextracted from container. - โ
DependencyTrackerextracted from container. - โ
ContainerMetricswith concrete state. - โ Unit tests for each helper component.
- โ
-
Phase D โ Container API Updates
- โ
Idempotent registration APIs with
RegistrationKey. - โ
Public
prepare()with fail-fast behavior. - โ
Delete legacy bootstrap state and
resolve_all(). - โ Unit tests for idempotency and conflicts.
- โ
Idempotent registration APIs with
-
Phase E โ Integration Adoption
- โ
ApplicationBootstrap uses
BootstrapRuntimedirectly. - โ
Annotation processing builds
BootstrapPlan. - โ Integration tests verify re-bootstrap and failures.
- โ
ApplicationBootstrap uses
-
Phase F โ Documentation and Cleanup
- โ Update TD-010 and user guides.
- โ Remove deprecated API references.
- โ Final documentation sweep.
๐ 12. Changelog¶
-
2025-10-10 โ Phase E implementation complete:
- Updated ApplicationBootstrap to use
BootstrapRuntimedirectly with modern bootstrap patterns - Updated annotation processing to build
BootstrapPlanwith registration steps - Created comprehensive integration tests for re-bootstrap idempotency and failure propagation
- Updated all user guides with TD-020 integration examples and modern bootstrap patterns
- Created comprehensive API reference documentation for bootstrap components
- Updated dependency injection roadmap to mark TD-020 as completed
- All integration tests pass with new bootstrap patterns
- Documentation now fully reflects TD-020 completion with comprehensive user guides and API references
- Updated ApplicationBootstrap to use
-
2025-10-09 โ Phase D implementation complete:
- Refactored
DiContainerto use_CoreContainer,ServiceResolver,RegistrationValidator,DependencyTrackerhelper components - Added
_dependency_graphproperty exposingDependencyGraphfor external bootstrap validation - Implemented idempotent registration APIs using
RegistrationKeywith O(1) checks for all registration methods - Added public
prepare()method with fail-fast singleton hydration - Removed legacy APIs:
resolve_all(),bootstrap(),_bootstrap_manager,_bootstrap_lock,_status,_ensure_bootstrapped - Updated
metricsproperty to returnContainerMetricsinstead ofDiMetrics - Updated
ConstructorInjectorto callresolve()instead of removed_resolve_service() - Created 25 new unit tests covering idempotent registrations, cross-type conflicts, and
prepare()behavior (100% pass rate) - All tests pass (
make test) and complexity/lint checks pass (make -k lint) - Note: This is a breaking change - existing code using
bootstrap()orresolve_all()will need updates in Phase E
- Refactored
- 2025-10-09 โ Phase C implementation complete:
- Implemented
ContainerMetricswith snapshot-based observability for registrations, resolutions, cache performance - Implemented
RegistrationValidatorusing reflection for type assignment validation - Implemented
ServiceResolvercoordinating resolution pipeline (cache check, creation, injection, caching) - Implemented
DependencyTrackerusing ConstructorInjector to populate dependency graph - Created 13 unit tests covering all Phase C components (100% pass rate)
- Total 601 tests passing (13 new tests for Phase C)
- All complexity checks passing
- All components in loryth.common.di.internal module
- Implemented
- 2025-10-09 โ Phase B implementation complete:
- Implemented
RegistrationKeywith id()-based identity for O(1) idempotency checks - Implemented
RegistrationKindenum for discriminating registration types - Implemented
DiContainerItemprotocol with three concrete implementations:TypeBasedRegistrationfor singleton/transient with provider functionsFactoryRegistrationfor factory-based service creationInstanceRegistrationfor pre-created instances
- Implemented
_CoreContainerwith dual-index structure (service_type and RegistrationKey) - Implemented
DependencyGraphusingAdjacencyListGraphfrom collections - Created 46 unit tests covering all Phase B components (100% pass rate)
- All 588 tests pass and complexity checks pass
- Components located in
loryth.common.di.internalmodule
- Implemented
- 2025-10-09 โ Phase A implementation complete:
- Implemented
BootstrapRuntimewith three-phase execution (registration, validation, preparation) - Implemented
BootstrapPlanas immutable dataclass with registration steps and control flags - Implemented
BootstrapMetricswith cumulative statistics tracking across all bootstrap executions - Implemented
DependencyAnalyzerusingloryth.common.collections.topological_sortfor cycle detection - Implemented registration step protocol with four concrete implementations (singleton, transient, factory, instance)
- Added
topological_sorttoloryth.common.collectionspublic API - Created 44 unit tests covering all Phase A components (100% pass rate)
- All tests pass (
make test) and complexity checks pass (make -k complexity-lint) - Note: mypy shows import errors for
loryth.common.di.di_containerandloryth.common.di.container.graphwhich will be resolved in Phase B/C
- Implemented
- 2025-10-09 โ Comprehensive refinement pass addressing structural completeness:
- Updated section 3.6 (Removal/Deprecation Policy) to reference 6-phase rollout structure
- Updated section 4.2 (DependencyAnalyzer) to use
loryth.common.collections.topological_sortdirectly - Completed section 4.3 with full implementations of
FactoryRegistrationandInstanceRegistration - Added section 4.3 helper component implementations (
RegistrationValidator,ServiceResolver,DependencyTracker) - Enhanced section 5 (Thread Safety) to explicitly document private
RLockencapsulation - Added ๐งช emoji to section 7 header for visual indicator consistency
- Completely rewrote section 13 (Glossary) with comprehensive term coverage and removed obsolete entries
- 2025-10-09 โ Major revision based on implementation feedback and container refactoring:
- Added comprehensive idempotency rules (section 3.4) for all registration types including cross-type conflicts
- Added metrics design (section 3.5) with
ContainerMetricsandBootstrapMetricsas concrete state trackers - Designed internal container architecture (section 4.3):
_CoreContainerfor O(1) lookups with dual-index structureDiContainerItemprotocol with discriminated union (TypeBasedRegistration,FactoryRegistration,InstanceRegistration)RegistrationKeyfor idempotency withid()-based identityDependencyGraphusingloryth.common.collections.AdjacencyListGraph- Helper components:
RegistrationValidator,ServiceResolver,DependencyTracker
- Removed
BootstrapLockPool- container owns thread-safety lock - Removed
bootstrap_container()helper - callers useBootstrapRuntimedirectly - Removed
resolve_all()- one service type = one registration - Changed registration steps from ABC to Protocol (section 4.1)
- Changed
prepare()to fail-fast instead of aggregating errors - Changed
DependencyAnalyzerto operate directly onDependencyGraph - Updated test matrix, acceptance criteria, rollout phases, risks, and progress tracking
- Updated all examples to reflect new API patterns
- 2025-10-08 โ Initial draft proposing external bootstrap runtime and phased migration.
๐ 13. Glossary¶
| Term | Definition |
|---|---|
| BootstrapRuntime | External orchestration layer coordinating container bootstrap operations (registration, validation, preparation). |
| BootstrapPlan | Immutable specification of registration steps and validation/preparation flags. |
| BootstrapMetrics | Runtime-owned metrics tracking bootstrap executions, scanned modules, validation results, and timings. |
| ContainerMetrics | Container-owned metrics tracking registrations, resolutions, cache performance, and operation timings. |
| RegistrationStep | Protocol for type-safe container registration operations (singleton, transient, factory, instance). |
| DependencyAnalyzer | Component validating dependency graphs using topological sort from loryth.common.collections. |
| DependencyGraph | Graph-based dependency tracker using AdjacencyListGraph for cycle detection. |
| _CoreContainer | Internal low-level registry providing O(1) lookups with dual-index structure. |
| DiContainerItem | Protocol for container registrations with discriminated union pattern. |
| RegistrationKey | Hashable key for O(1) idempotency checks using id()-based identity. |
| RegistrationValidator | Helper component validating type assignments using reflection. |
| ServiceResolver | Helper component managing resolution, caching, and injection pipeline. |
| DependencyTracker | Helper component analyzing constructors and populating dependency graph. |
| Idempotency | Property where repeated identical operations produce no observable change. |
| Fail-Fast | Error handling strategy where operations raise on first error rather than aggregating failures. |
| Cross-Type Conflict | Error condition where same service type has multiple registration methods (singleton + factory, etc.). |