Skip to content

๐Ÿ“˜ 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:

  • DiContainer becomes bootstrap-agnostic โ€” no embedded BootstrapManager, no self-triggered bootstrap.
  • New BootstrapRuntime encapsulates planning, dependency analysis, and observability, then calls container.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: DiContainer owns BootstrapManager, 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

  1. Externalize Orchestration โ€” Container exposes only registration/prepare APIs; bootstrap coordination lives entirely in the runtime.
  2. Idempotent Bootstrap โ€” Re-running bootstrap with the same inputs performs no observable change (deterministic registrations + idempotent container).
  3. Unified Bootstrap API โ€” Provide a single entry point (BootstrapRuntime.bootstrap(container, plan)) used by Application Bootstrap, tests, and tooling.
  4. Observability Preservation โ€” Retain existing metrics/events (DiMetrics) while adding runtime-level bootstrap metrics.
  5. Thread-Safe Operation โ€” Maintain current concurrency guarantees (RLock double-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.
  • DiContainer can be updated to expose idempotent registration semantics, prepare(), and iter_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 DiRegistrationError with 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 BootstrapRuntime directly.
  • 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_* and resolve operations 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 _CoreContainer indices
  • โœ… 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: DiContainer owns a private RLock (internal implementation detail) that guards all registration and resolution operations
  • Automatic Acquisition: All register_* and resolve methods acquire the lock internally
  • No Runtime Locks: BootstrapRuntime does 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 DiRegistrationError for 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() raises BootstrapError on cycles or missing dependencies
  • Preparation Failures: container.prepare() raises resolution errors directly (wrapped in DiResolutionError)
  • 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 RegistrationKey and conflict detection via conflicts_with().
  • Graph Operations: DependencyGraph correctly adds nodes/edges using AdjacencyListGraph.

7.2 Integration Coverage

  • Application Bootstrap Flow: ApplicationBootstrap + runtime produce a configured container and invoke prepare().
  • 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

  • DiContainer no longer instantiates or owns BootstrapManager; uses _CoreContainer and helper components.
  • DiContainer registrations are idempotent with O(1) checks via RegistrationKey.
  • DiContainer.prepare() publicly exposed and fails fast on first singleton error.
  • DependencyGraph uses loryth.common.collections.AdjacencyListGraph for dependency tracking.
  • BootstrapRuntime + BootstrapPlan implemented with metrics and DependencyAnalyzer integration.
  • DependencyAnalyzer.validate() operates directly on DependencyGraph (not dict conversion).
  • Legacy APIs (DiContainer.bootstrap(), implicit auto-bootstrap) removed entirely.
  • Application bootstrap (TD-013) and annotation processing use BootstrapRuntime directly.
  • 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

  1. Phase A โ€“ Runtime Foundations

    • Implement BootstrapRuntime, BootstrapPlan, and registration step subclasses.
    • Implement BootstrapMetrics with concrete state tracking (no sink pattern).
    • Implement DependencyAnalyzer operating directly on DependencyGraph.
    • Add unit tests covering deterministic plans, dependency validation, and metrics emission.
  2. Phase B โ€“ Container Internal Refactoring

    • Implement _CoreContainer with dual-index structure (_current, _registered_keys).
    • Implement DiContainerItem protocol with discriminated union (TypeBasedRegistration, etc.).
    • Implement RegistrationKey with id()-based identity fields.
    • Implement DependencyGraph using loryth.common.collections.AdjacencyListGraph.
    • Add unit tests for _CoreContainer, DependencyGraph, and container items.
  3. Phase C โ€“ Helper Component Extraction

    • Extract RegistrationValidator from container validation logic.
    • Extract ServiceResolver from resolution logic.
    • Extract DependencyTracker from dependency analysis logic.
    • Implement ContainerMetrics with concrete state (registrations, resolutions, cache hits).
    • Refactor DiContainer to use helpers and _CoreContainer.
    • Add unit tests for each helper component.
  4. Phase D โ€“ Container API Updates

    • Make registration APIs idempotent using RegistrationKey checks.
    • Implement public prepare() with fail-fast behavior.
    • Expose metrics property returning ContainerMetrics snapshot.
    • Delete _bootstrap_manager, _bootstrap_lock, _status, _ensure_bootstrapped.
    • Delete resolve_all() method.
    • Add unit tests for idempotent registrations and cross-type conflict detection.
  5. Phase E โ€“ Integration Adoption

    • Update ApplicationBootstrap to use BootstrapRuntime.bootstrap() directly.
    • Update annotation processing to build BootstrapPlan with registration steps.
    • Update all integration tests to call runtime directly (no helper).
    • Add integration tests for idempotent re-bootstrap and failure propagation.
  6. 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

    • โœ… BootstrapRuntime orchestrates deterministic registrations.
    • โœ… BootstrapPlan with registration step protocol.
    • โœ… BootstrapMetrics with concrete state tracking.
    • โœ… DependencyAnalyzer operates on DependencyGraph directly.
    • โœ… Unit tests cover plans, validation, and metrics.
  • Phase B โ€” Container Internal Refactoring

    • โœ… _CoreContainer with O(1) dual-index structure.
    • โœ… DiContainerItem protocol with discriminated union.
    • โœ… RegistrationKey with id()-based identity.
    • โœ… DependencyGraph using AdjacencyListGraph.
    • โœ… Unit tests for core container and graph.
  • Phase C โ€” Helper Component Extraction

    • โœ… RegistrationValidator extracted from container.
    • โœ… ServiceResolver extracted from container.
    • โœ… DependencyTracker extracted from container.
    • โœ… ContainerMetrics with 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.
  • Phase E โ€” Integration Adoption

    • โœ… ApplicationBootstrap uses BootstrapRuntime directly.
    • โœ… Annotation processing builds BootstrapPlan.
    • โœ… Integration tests verify re-bootstrap and failures.
  • 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 BootstrapRuntime directly with modern bootstrap patterns
    • Updated annotation processing to build BootstrapPlan with 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
  • 2025-10-09 โ€” Phase D implementation complete:

    • Refactored DiContainer to use _CoreContainer, ServiceResolver, RegistrationValidator, DependencyTracker helper components
    • Added _dependency_graph property exposing DependencyGraph for external bootstrap validation
    • Implemented idempotent registration APIs using RegistrationKey with 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 metrics property to return ContainerMetrics instead of DiMetrics
    • Updated ConstructorInjector to call resolve() 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() or resolve_all() will need updates in Phase E
  • 2025-10-09 โ€” Phase C implementation complete:
    • Implemented ContainerMetrics with snapshot-based observability for registrations, resolutions, cache performance
    • Implemented RegistrationValidator using reflection for type assignment validation
    • Implemented ServiceResolver coordinating resolution pipeline (cache check, creation, injection, caching)
    • Implemented DependencyTracker using 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
  • 2025-10-09 โ€” Phase B implementation complete:
    • Implemented RegistrationKey with id()-based identity for O(1) idempotency checks
    • Implemented RegistrationKind enum for discriminating registration types
    • Implemented DiContainerItem protocol with three concrete implementations:
      • TypeBasedRegistration for singleton/transient with provider functions
      • FactoryRegistration for factory-based service creation
      • InstanceRegistration for pre-created instances
    • Implemented _CoreContainer with dual-index structure (service_type and RegistrationKey)
    • Implemented DependencyGraph using AdjacencyListGraph from 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.internal module
  • 2025-10-09 โ€” Phase A implementation complete:
    • Implemented BootstrapRuntime with three-phase execution (registration, validation, preparation)
    • Implemented BootstrapPlan as immutable dataclass with registration steps and control flags
    • Implemented BootstrapMetrics with cumulative statistics tracking across all bootstrap executions
    • Implemented DependencyAnalyzer using loryth.common.collections.topological_sort for cycle detection
    • Implemented registration step protocol with four concrete implementations (singleton, transient, factory, instance)
    • Added topological_sort to loryth.common.collections public 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_container and loryth.common.di.container.graph which will be resolved in Phase B/C
  • 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_sort directly
    • Completed section 4.3 with full implementations of FactoryRegistration and InstanceRegistration
    • Added section 4.3 helper component implementations (RegistrationValidator, ServiceResolver, DependencyTracker)
    • Enhanced section 5 (Thread Safety) to explicitly document private RLock encapsulation
    • 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 ContainerMetrics and BootstrapMetrics as concrete state trackers
    • Designed internal container architecture (section 4.3):
      • _CoreContainer for O(1) lookups with dual-index structure
      • DiContainerItem protocol with discriminated union (TypeBasedRegistration, FactoryRegistration, InstanceRegistration)
      • RegistrationKey for idempotency with id()-based identity
      • DependencyGraph using loryth.common.collections.AdjacencyListGraph
      • Helper components: RegistrationValidator, ServiceResolver, DependencyTracker
    • Removed BootstrapLockPool - container owns thread-safety lock
    • Removed bootstrap_container() helper - callers use BootstrapRuntime directly
    • 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 DependencyAnalyzer to operate directly on DependencyGraph
    • 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.).