π Technical Design 011 β DI Injection System¶
| Field | Value |
|---|---|
| ID | TD-011 |
| Title | DI Injection System |
| Status | Completed |
| Owner | Contributors |
| Audience | Contributors, maintainers, DI system users |
| Created | 2025-10-02 |
| Last Updated | 2025-10-06 |
| Reviewers | TBD |
| Approvers | TBD |
| Tracking | Phase 3 of DI Roadmap |
| Related | 001-dependency-injection-roadmap.md |
| 010-di-bootstrapping-initialization.md |
π§ 1. Summary¶
This technical design advances Phase 3 of the DI roadmap by expanding the existing DiContainer with a unified injection system. Building on TD-010βs bootstrap/constructor infrastructure, the design clarifies how property injection, proxy-backed method interception, and custom cross-cutting injectors combineβleveraging the existing loryth.common.annotations utilities where desired.
Key Features: - Unified Injector Pipeline: All injectors run for every resolved instance in deterministic priority order - Property Injection: Autowired[T] typing annotation drives AutowiredInjector without extra decorators - Proxy Utility: InjectionProxyFactory (new in TD-011) centralises proxy creation for method-wrapping injectors - Custom Injectors: Injectors can use loryth.common.annotations.AnnotationContext today; TD-012 only adds auto-registration conveniences - Builder Pattern: DiContainerBuilder keeps sensible defaults while exposing injector customization - TD-010 Integration: Resolution and bootstrap share metrics, cycle detection, and error handling - Enhanced Error Handling: Expanded InjectorError hierarchy distinguishes constructor, property, method, and pipeline failures
π 2. Context and Problem Statement¶
The Phase 1-2 container provides registration, resolution, and bootstrapping (from TD-010) with basic constructor injection for services, but lacks advanced injection capabilities:
Current State and Limitations¶
Current Constructor Injection: - Services registered via register_singleton()/register_transient() get automatic constructor injection - Factories registered via register_factory() use provided factory functions - Implemented in _create_instance_with_dependencies() using reflection analysis
Missing Capabilities: 1. No Property Injection - No automatic injection into service properties after creation 2. No Cross-Cutting Concerns - No support for AOP-style method interception 3. No Injector Registry - No API for custom injection behavior registration 4. Limited Error Handling - Basic errors, no injection-specific error types
2.1 Goals¶
- Enhanced Constructor Injection - Improve existing constructor injection with better error handling
- Property Injection - Automatic injection into marked properties after creation using annotation-based discovery
- Method Wrapping - Support for cross-cutting concerns via proxy-based method interception; annotation discovery will piggyback on TD-012
- Injector Registry API - Registration system for custom injection behaviors with priority-based execution
- Lifecycle Integration - Injection happens at appropriate points in service lifecycle, integrated with TD-010 bootstrap
2.2 Non-Goals (Out of Scope)¶
- Full AOP framework implementation (limited to method wrapping)
- Field injection (prefer property injection for clarity)
- Named injection or qualifiers (Phase 4+)
- Conditional injection based on environment (Phase 4+)
- Generic type injection (out of scope)
2.3 Assumptions and Constraints¶
- Phase 1-2 DiContainer implementation with basic constructor injection is available
- TD-010 bootstrap infrastructure is complete and integrated
- Reflection system provides constructor and property analysis
- Type annotations used for property injection discovery
loryth.common.annotationsprimitives are available for injectors to inspect methods today; TD-012 focuses on auto-registration and decoration ergonomics- Method wrapping relies on explicit injector configuration combined with
InjectionProxyFactory
π§© 3. Design Overview¶
The injection system enhances the existing container constructor injection and adds two new injection types: post-creation property injection and runtime method wrapping. The design integrates with TD-010's bootstrap process and reuses its cycle detection mechanism.
graph TD
A[Service Resolution] --> B{Lifecycle}
B -->|Singleton / Transient| C[Registered Provider]
B -->|Factory| D[Factory Provider]
C --> E[Create Instance: constructor injection]
D --> E
E --> F[Injector Pipeline]
F --> G[AutowiredInjector βΊ resolves typed properties]
F --> H[Custom Injectors βΊ optional proxy/wrappers]
G --> I[Augmented Instance]
H --> I
I --> J[Return to Caller]
classDef lifecycle fill:none,stroke:#2196f3,stroke-width:2px,color:#ffffff
classDef processNode fill:none,stroke:#4caf50,stroke-width:2px,color:#ffffff
classDef resultNode fill:none,stroke:#9c27b0,stroke-width:2px,color:#ffffff
class B lifecycle
class C,D,E,F,G,H processNode
class A,J resultNode 3.1 Components Overview¶
| Component | Purpose | Location |
|---|---|---|
InjectionManager | Orchestrates unified injector execution pipeline | src/loryth/common/di/injection/manager.py |
AutowiredInjector | Pre-built injector for Autowired[T] property injection | src/loryth/common/di/injection/autowired.py |
InjectionProxyFactory | Utility that builds interception proxies for method injectors | src/loryth/common/di/injection/proxy_factory.py |
InjectorRegistry | Manages registration of custom injectors with priority | src/loryth/common/di/injection/registry.py |
Autowired[T] | Type annotation for marking injection points | src/loryth/common/di/injection/autowired.py |
Injector | Protocol for custom injectors | src/loryth/common/di/injection/injector.py |
InjectorError | Exception hierarchy for injection-specific failures | src/loryth/common/di/injection/exceptions.py |
3.2 Integration with TD-010¶
The injection system builds on TD-010's bootstrap infrastructure:
- Cycle Detection: Reuses TD-010's
DependencyCycleDetector(constructor injection already integrated) - Bootstrap Integration: Property and method injection integrate with bootstrap lifecycle
- Error Handling:
InjectorErrorwrapped inBootstrapErrorduring bootstrap failures - State Management: Respects container status from TD-010 (READY/PENDING/FAILED)
3.3 Injection Control and Registration¶
Injection behavior is controlled through unified injector registration:
- Constructor Injection: Already implemented for services (
register_singleton/register_transient), not for factories (register_factory) - Post-Creation Injection: All registered injectors run for all instances in priority order
- Default Configuration:
AutowiredInjectorincluded by default viaDiContainerBuilder - Custom Injectors: Register additional injectors via
register_injector()API - State Isolation: Injectors don't share state except container access
3.4 Injection Timing¶
- Singleton Services: Injection happens during bootstrap (fail-fast approach)
- Transient Services: Injection happens on-demand during each
resolve()call - Factories: Use provided factory functions, no automatic injection
π 4. Implementation Details¶
4.1 Container Integration¶
DiContainer (see src/loryth/common/di/container.py) now composes three collaborators: a ConstructorInjector for constructor analysis, an InjectorRegistry for ordered injector execution, and an InjectionManager that orchestrates post-construction pipelines. Key touch points:
__init__wires_constructor_injector,_injector_registry,_injection_manager, and_optional_dependencies, preserving TD-010 bootstrap/metrics plumbing._create_singleton_provider()/_create_transient_provider()delegate instance creation toConstructorInjector, guaranteeing consistent analysis across lifecycles._analyze_and_track_dependencies()stores both required and optional constructor dependencies so cycle detection has full visibility while bootstrap skips optional fallbacks that rely on defaults._resolve_service()still routes provider outputs throughInjectionManager.create_with_injection()so property/method injectors run for singletons, transients, factories, and pre-registered instances.register_instance()hydrates pre-built singletons immediately by calling_apply_all_injectors()before caching.
The builder (src/loryth/common/di/di_container_builder.py) continues to seed AutowiredInjector by default and exposes with_injector() for additional behaviours before calling DiContainer(...).
class DiContainer:
def __init__(self, registry: InjectorRegistry | None = None) -> None:
self._constructor_injector = ConstructorInjector(self)
self._injector_registry = registry or InjectorRegistry()
self._injection_manager = InjectionManager(self)
self._optional_dependencies: dict[type, list[type]] = {}
def _create_singleton_provider(self, impl_type):
return lambda _: self._constructor_injector.create(impl_type)
def _analyze_and_track_dependencies(self, service_type, impl_type) -> None:
metadata = self._constructor_injector.analyze(impl_type)
required = [dep for dep, is_optional in metadata if not is_optional]
optional = [dep for dep, is_optional in metadata if is_optional]
self._dependencies[service_type] = [*required, *(dep for dep in optional if dep not in required)]
self._optional_dependencies[service_type] = optional
4.2 ConstructorInjector¶
ConstructorInjector (src/loryth/common/di/injection/constructor.py) centralises constructor analysis:
- Normalises
Annotated,Optional, and single-branchUnionannotations to concrete dependency types. - Treats optional annotations and defaulted parameters as soft dependencies so bootstrap skips them when no provider exists, yet still resolves them when services are registered.
- Delegates to the container for dependency resolution, preserving cycle detection and fail-fast semantics.
- Caches per-type parameter plans so repeated resolutions avoid recomputing reflection metadata.
class ConstructorInjector:
def analyze(self, impl: type[Any]) -> list[tuple[type[Any], bool]]:
plan = self._ensure_plan(impl)
return [(item.service_type, item.optional or item.has_default) for item in plan if item.service_type]
def create(self, impl: type[T]) -> T:
plan = self._ensure_plan(impl)
args: list[Any] = []
for item in plan:
value = self._resolve_parameter(impl, item)
args.append(value) if item.kind in {"POSITIONAL_ONLY", "POSITIONAL_OR_KEYWORD"} else None
return impl(*args)
4.3 Unified Injection Pipeline (InjectionManager)¶
InjectionManager (src/loryth/common/di/injection/manager.py) is the single entry point for injection orchestration:
- Invoke the provider stored in
DiContainerItem(constructor-based providers, factory callables, or pre-registered instances). - Iterate over the registry in ascending priority, passing the current target and container to each injector.
- Surface injector-specific failures unchanged (
InjectorError) and re-wrap unexpected exceptions asInjectorPipelineErrorwith the target name for context.
Because every resolution path flows through this manager, custom injectors inherit consistent ordering, error handling, and tracing semantics.
class InjectionManager:
def create_with_injection[T](self, item: DiContainerItem[T]) -> object:
target = item.provider(self._container)
return self._apply_all_injectors(target)
def _apply_all_injectors(self, target: object) -> object:
for injector in self._container._injector_registry.get_injectors():
target = injector.inject(target, self._container)
return target
4.4 AutowiredInjector Implementation¶
AutowiredInjector (src/loryth/common/di/injection/autowired.py) implements property injection by scanning __annotations__ for Autowired[T] markers. For each match it:
- Extracts the generic argument (
T) and resolves it through the container. - Assigns the resolved dependency directly onto the instance.
- Raises
PropertyInjectionErrorif the annotation is malformed or resolution fails.
This injector currently supports only direct Autowired[T] annotations; wrappers such as Optional[Autowired[T]] are treated as unsupported until the constructor-injection refactor extends annotation analysis across the board.
class AutowiredInjector:
def inject(self, target: object, container: DiContainer) -> object:
for name, annotation in getattr(target.__class__, "__annotations__", {}).items():
if getattr(annotation, "__origin__", None) is Autowired:
dependency_type = annotation.__args__[0]
setattr(target, name, container.resolve(dependency_type))
return target
4.5 InjectionProxyFactory¶
InjectionProxyFactory (src/loryth/common/di/injection/proxy_factory.py) gives method-oriented injectors a reusable way to compose decorators without hand-written proxy types:
- Cache: builds a lightweight subclass per concrete target type and reuses it for subsequent wraps.
- Bindings: accepts a
dict[str, Callable]mapping method names to interceptors, validates callable returns, and binds wrappers with optional signature preservation (functools.wraps). - Invocation: when an intercepted attribute is accessed, the proxy lazily materialises the wrapped method, caches it on the proxy, and delegates all other attributes directly to the target.
- Errors: non-callable interceptors or wrappers that return non-callables trigger
MethodInjectionErrorwith detailed signature mismatch information.
Injectors using this factory only need to describe what to intercept; the factory handles proxy lifecycle, caching, and metadata hygiene.
import logging
factory = InjectionProxyFactory()
logger = logging.getLogger(__name__)
def audit(original, obj, *args, **kwargs):
log_access(obj)
return original(*args, **kwargs)
def log_access(obj: object) -> None:
logger.info("Calling %s", obj.__class__.__name__)
proxy = factory.wrap(
target,
{
"process": lambda original, obj: lambda *args, **kwargs: audit(original, obj, *args, **kwargs),
},
)
4.6 Injector Registry¶
InjectorRegistry (src/loryth/common/di/injection/registry.py) stores (priority, injector) tuples, keeps the list sorted (lower priority executes earlier), and exposes register(), get_injectors(), clear(), and count(). The registry is intentionally minimal so different container entry points (builder vs. runtime registration) share the same behaviour and ordering semantics.
4.7 Error Model¶
The injection layer centralises its failures under loryth.common.di.exceptions:
InjectorError: base type for any injector failure.ConstructorInjectionError: raised when constructor parameter resolution or instantiation fails.PropertyInjectionError: emitted byAutowiredInjectorwhen annotations or assignments misbehave.MethodInjectionError: produced byInjectionProxyFactoryor method injectors when interceptors are invalid.InjectorPipelineError: wraps unexpected exceptions thrown while iterating the registry.
Bootstrap and resolution both catch these errors and wrap them in the existing BootstrapError / DiResolutionError so callers get high-level context while the original injector failure remains in __cause__ for diagnostics.
4.8 Example: Annotation-Driven Method Injector¶
Custom injectors can already combine AnnotationContext with InjectionProxyFactory to wrap annotated methods (TD-012 will add auto-registration, not new core capabilities):
from contextlib import contextmanager
from typing import Any, Callable
from loryth.common.annotations import AnnotationContext
from loryth.common.di.container import Container
from loryth.common.di.injection import InjectionProxyFactory, Injector
class TransactionalInjector(Injector):
def __init__(self) -> None:
self._proxy_factory = InjectionProxyFactory()
self._annotation = AnnotationContext("transactional")
def inject(self, target: object, container: Container) -> object:
interceptors: dict[str, Callable[..., Any]] = {}
for method in self._annotation.iterate_methods(target):
interceptors[method.name] = self._make_interceptor()
return self._proxy_factory.wrap(target, interceptors)
@staticmethod
def _make_interceptor() -> Callable[..., Any]:
def decorator(original: Callable[..., Any], obj: object) -> Callable[..., Any]:
def wrapped(*args: Any, **kwargs: Any) -> Any:
return run_transaction(lambda: original(*args, **kwargs))
return wrapped
return decorator
def run_transaction(callback: Callable[[], Any]) -> Any:
with transaction_manager():
return callback()
@contextmanager
def transaction_manager():
begin_transaction()
try:
yield
commit_transaction()
except Exception:
rollback_transaction()
raise
def begin_transaction() -> None: ...
def commit_transaction() -> None: ...
def rollback_transaction() -> None: ...
run_transaction() demonstrates the expected injector contract: open/commit/rollback around the original method so consumers can opt into transactional behaviour today without waiting for TD-012 automation.
βοΈ 5. Semantics¶
5.1 Injection Order¶
- Constructor Injection: During instance creation (already implemented in provider functions)
- Injector Execution: All registered injectors run in priority order after instance creation
5.2 Injection Rules¶
- Constructor: Automatic for services (
register_singleton/register_transient), factories use provided functions - Injector Execution: All registered injectors process all instances, each injector decides what to modify
- Default Behavior:
AutowiredInjectorhandlesAutowired[T]properties, custom injectors handle their specific concerns - Optional Constructor Parameters:
Optional[...]or defaulted parameters fall back to their default when no provider exists; when registered the container resolves them throughConstructorInjector
5.3 Lifecycle Integration¶
- Constructor Injection: Already integrated in existing provider creation
- Injector Execution: Registered via
register_injector()API orDiContainerBuilder, apply during resolution - Singleton Behavior: Injection happens once during bootstrap (fail-fast approach)
- Transient Behavior: Injection happens on-demand during each
resolve()call - Pre-Registered Instances:
register_instanceruns the unified pipeline during registration and caches the injected instance so subsequent resolves reuse the hydrated singleton - Bootstrap Integration: All injection integrates with TD-010 bootstrap process
5.4 Error Handling¶
- InjectorError: Base exception for injection-specific failures
- Bootstrap Integration:
InjectorErrorwrapped inBootstrapErrorduring bootstrap failures - Fail-Fast Approach: Constructor injection already fails fast with existing error handling
- Cycle Detection: Already integrated via TD-010's cycle detection for constructor dependencies
π§ͺ 6. Examples¶
βΉοΈ Examples 6.3 and 6.4 demonstrate how injectors can pair
AnnotationContextscanning withInjectionProxyFactorytoday. TD-012 will add syntactic sugar (e.g., declarative decorators) on top of this capability. π Runnable reference injectors live undertests/resources/di/injection/reference_injectors.py. They mirror the concepts below without expanding the production API surface.
6.1 Constructor Injection¶
class UserService:
def __init__(self, repository: UserRepository, logger: Logger) -> None:
self.repository = repository
self.logger = logger
# Registration
container.register_singleton(UserRepository, SqlUserRepository)
container.register_singleton(Logger, FileLogger)
container.register_singleton(UserService, UserService)
# Resolution with existing constructor injection
user_service = container.resolve(UserService)
# UserService created with SqlUserRepository and FileLogger injected (already implemented)
6.2 Property Injection with AutowiredInjector¶
from loryth.common.di.injection import Autowired
class OrderService:
# Primary usage: Type annotation only (recommended)
user_service: Autowired[UserService]
config: Autowired[AppConfig]
def create_order(self, user_id: int) -> Order:
user = self.user_service.get_user(user_id) # Automatically injected
return Order(user, self.config.tax_rate) # Automatically injected
# Registration
container = DiContainerBuilder().build() # AutowiredInjector included by default
container.register_singleton(UserService, UserService)
container.register_singleton(AppConfig, AppConfig)
container.register_singleton(OrderService, OrderService)
# Resolution with automatic property injection via AutowiredInjector
order_service = container.resolve(OrderService)
# Properties automatically injected by AutowiredInjector after creation
6.3 Method Wrapping with Custom Injectors¶
from tests.resources.di.injection.reference_injectors import CachingInjector
from tests.resources.di.injection.reference_injectors import TransactionalInjector
from loryth.common.annotations import annotator
class TransactionService:
@transactional(isolation_level="SERIALIZABLE") # Runtime annotation
def update_user(self, user: User) -> None:
# Method automatically wrapped with transaction logic
user.update_last_login()
@cached(ttl=300) # Another annotation example
def get_user_stats(self, user_id: int) -> UserStats:
# Method wrapped with caching logic
return self._calculate_stats(user_id)
# Create container with custom injectors
container = (DiContainerBuilder()
.with_injector(TransactionalInjector()) # Custom injector for @transactional
.with_injector(CachingInjector()) # Custom injector for @cached
.build()) # AutowiredInjector included by default
# Register service
container.register_singleton(TransactionService, TransactionService)
# Resolution: all injectors run, TransactionalInjector creates proxy with wrapped methods
service = container.resolve(TransactionService)
service.update_user(user) # Runs inside transaction automatically
6.4 Custom Injector Example¶
from functools import wraps
from typing import Callable, Any
from loryth.common.annotations import Annotation
from loryth.common.annotations import AnnotationContext
from loryth.common.annotations import annotator
from loryth.common.di.injection import InjectionProxyFactory
class Injector(Protocol):
"""Protocol for custom injectors that can transform instances."""
def inject(self, target: object, container: "DiContainer") -> object:
"""Transform the target object (wrapping, modification, etc.) and return result."""
...
# Example: Transactional annotation and injector
class TransactionalAnnotation(Annotation):
def __init__(self, isolation_level: str = "READ_COMMITTED") -> None:
super().__init__("transactional")
self.isolation_level = isolation_level
# Create decorator using the annotation system
transactional = annotator(lambda isolation_level="READ_COMMITTED": TransactionalAnnotation(isolation_level))
class TransactionalInjector:
"""Example injector that wraps methods with @transactional annotations."""
def __init__(self) -> None:
self._proxy_factory = InjectionProxyFactory()
def inject(self, target: object, container: "DiContainer") -> object:
"""Transform target by wrapping transactional methods."""
# Find methods with @transactional annotations
interceptors: dict[str, Callable[..., Any]] = {}
for attr_name in dir(target):
if not attr_name.startswith('_'):
attr = getattr(target, attr_name)
if callable(attr) and AnnotationContext.has_annotation(attr, TransactionalAnnotation):
annotation = AnnotationContext.find_annotation(attr, TransactionalAnnotation)
@wraps(attr)
def transactional_method(*args: object, __method: Callable[..., Any] = attr, __annotation: TransactionalAnnotation = annotation, **kwargs: object) -> Any:
tx_manager = container.resolve(TransactionManager)
try:
tx_manager.begin(isolation_level=__annotation.isolation_level)
result = __method(*args, **kwargs)
tx_manager.commit()
return result
except Exception:
tx_manager.rollback()
raise
interceptors[attr_name] = transactional_method
if not interceptors:
return target # No wrapping needed
return self._proxy_factory.wrap(target, interceptors)
6.5 Integration with TD-010 Bootstrap¶
# Reference injectors imported from tests/resources/di/injection/reference_injectors.py
from tests.resources.di.injection.reference_injectors import CachingInjector
from tests.resources.di.injection.reference_injectors import TransactionalInjector
# Register services with injection
container.register_singleton(Database, Database)
container.register_singleton(UserRepository, SqlUserRepository)
container.register_singleton(UserService, UserService)
# Create container with injectors (AutowiredInjector included by default)
container = (DiContainerBuilder()
.with_injector(TransactionalInjector())
.with_injector(CachingInjector())
.build())
# Manual bootstrap with injection validation
try:
container.bootstrap() # Validates dependencies and performs injection
print("Bootstrap successful - all dependencies injected")
except BootstrapError as e:
print(f"Bootstrap failed: {e}")
# InjectorError would be wrapped in the BootstrapError
except DependencyCycleError as e:
print(f"Circular dependency detected: {e}")
6.6 Reference Injector Modules¶
Sample implementations of transactional and caching injectors live under tests/resources/di/injection/reference_injectors.py. They provide concrete blueprints for building cross-cutting injectors with InjectionProxyFactory while remaining outside the production package surface.
β 7. Acceptance Criteria¶
Legend: β done Β· π§ in progress Β· π pending Β· β cancelled
7.1 Core Functionality¶
- β Constructor injection continues to work for services (singleton/transient) but not factories
- β
Property injection works with
Autowired[T]type annotation viaAutowiredInjector - β
Custom injectors can create proxies using
InjectionProxyFactory - β Injection works with both singleton and transient lifecycles
- β Property injection provides clear error messages for missing types
- β Method wrapping preserves original method signatures and metadata
- β Injection integrates seamlessly with existing resolve process
- β
Custom injectors can be registered via
register_injector()API with priority parameters - β
InjectorErrorexceptions are wrapped inBootstrapErrorduring bootstrap failures - β Injector registry manages injectors without shared state except container access
- β
Injectors can inspect runtime annotations via
AnnotationContext - β Annotation-driven auto-registration shortcuts (TD-012 future work)
- β
register_instanceflows through the unified pipeline so all injectors execute for cached instances
7.2 Test Coverage Requirements¶
Unit Test Coverage¶
- β
Constructor Injection Enhancement: Verified via
tests/integration/di/injection/test_td011_compliance.py::test_constructor_injection_works_for_services - β
Unified Injector Execution: Covered by
tests/unit/di/injection/test_injection_manager.py - β
AutowiredInjector: Covered by
tests/unit/di/injection/test_autowired.py - β
Custom Injectors:
tests/integration/di/injection/test_td011_compliance.py::test_annotation_driven_proxy_injector - β
Injector Registry: Covered by
tests/unit/di/injection/test_injector_registry.py - β
Error Handling:
tests/unit/di/injection/test_autowired.py::test_autowired_injector_raises_on_missing_service,tests/unit/di/injection/test_injection_manager.py::test_injection_manager_wraps_injector_failures_in_pipeline_error,tests/integration/di/injection/test_error_handling_integration.py::test_constructor_instantiation_error_wrapped_in_constructor_error
Integration Test Coverage¶
- β
Complete Workflows:
tests/integration/di/injection/test_td011_compliance.py::test_complete_workflow_registration_to_resolution - β
Unified Injection:
tests/integration/di/injection/test_td011_compliance.py::test_services_with_constructor_and_multiple_custom_injectors - β
Complex Dependencies:
tests/integration/di/injection/test_deep_dependency_graph.py::test_deep_dependency_graph_resolves_and_injects - β
TD-010 Integration:
tests/integration/di/injection/test_error_handling_integration.py - β
Optional/Annotated Constructors:
tests/integration/di/injection/test_td011_compliance.py::{test_constructor_injector_prefers_registered_optional_dependencies,test_constructor_injector_supports_annotated_dependencies} - β Performance: Large service graphs (50+ services), concurrent resolution, minimal overhead β coverage moved to TD-018 DI Benchmarking Suite
- β
Instance Registration Regression:
tests/integration/di/injection/test_td011_compliance.py::test_register_instance_runs_injection_pipeline
Test Organization¶
- β
Unit Tests:
tests/unit/di/injection/in place - β
Integration Tests:
tests/integration/di/injection/in place - β
Test Resources:
tests/resources/di/injection/deep_graph.py
π 8. Rollout and Migration¶
8.1 Implementation Phases¶
Phase 3.1 Β· Constructor Injection Infrastructure - β
Implement ConstructorInjector with parameter analysis - β
Add constructor injection to the resolution pipeline - β
Integrate with TD-010's cycle detection for constructor dependencies - β
Create the strengthened InjectionError hierarchy - β
Deliver basic reflection integration for constructor discovery
Phase 3.2 Β· Unified Injection System - β
Implement Autowired[T] type annotation and AutowiredInjector - β
Add InjectorRegistry with priority-based registration - β
Ensure the post-creation injection pipeline runs after every provider - π Integrate with the TD-012 annotation module for custom injector discovery - β
Refactor register_instance so cached instances traverse the pipeline
Phase 3.3 Β· Custom Injector Framework - β
Provide example custom injectors (transactional, caching) as reference implementations - β
Ship DiContainerBuilder with default injector configuration - β
Support proxy-driven cross-cutting injectors via InjectionProxyFactory - β
Maintain integration with the unified injection pipeline - π Optimise for low-change scenarios and measure overhead
Phase 3.4 Β· Integration and Error Handling - β Complete integration with TD-010 bootstrap process - β Enhance error handling with detailed diagnostics and tests - β Expand coverage to every injection type (method wrapping, register-instance regression) - π Run performance testing and optimisation passes - β Update documentation and examples once proxy tooling lands
8.2 Backward Compatibility¶
- All Phase 1-2 functionality remains unchanged
- Injection is automatic and transparent for compatible services
- Non-injectable services (factories) continue to work unchanged
- TD-010 bootstrap functionality remains unchanged
β οΈ 9. Risks and Mitigations¶
9.1 Performance Risk¶
Risk: Injection overhead for simple services that don't need it Mitigation: Optimize injection detection, provide opt-out mechanisms, lazy evaluation
9.2 Circular Dependency Risk¶
Risk: Constructor injection creating circular dependencies Mitigation: Reuse TD-010's cycle detection, fail-fast approach, clear error messages for missing dependencies
9.3 Reflection Dependency Risk¶
Risk: Heavy reliance on reflection system for injection Mitigation: Performance monitoring, optimization of annotation scanning
9.4 Complexity Risk¶
Risk: Increased complexity in container resolution logic Mitigation: Clear separation of concerns, comprehensive testing, detailed documentation
π 10. Progress¶
Legend: β done Β· π§ in progress Β· π pending
Phase 3.1: Constructor Injection Infrastructure¶
- β
Implement
ConstructorInjectorwith parameter analysis - β Add constructor injection to the resolution pipeline (existing providers reuse TD-010 infrastructure)
- β Integrate with TD-010's cycle detection
- β
Create
InjectionErrorexception hierarchy - β Basic reflection integration for constructor discovery
Phase 3.2: Unified Injection System¶
- β
Implement
Autowired[T]type annotation - β
Add
AutowiredInjectoras pre-built injector - β
Add
InjectorRegistrywith priority-based registration - π Integrate with the annotation system for custom injector discovery (TD-012 future work)
- β Integrate with post-creation injection pipeline
- β
Refactor
register_instanceso cached instances pass through the unified pipeline
Phase 3.3: Custom Injector Framework¶
- β Implement example custom injectors (transactional, caching, etc.)
- β
Add
DiContainerBuilderwith default injector configuration - β
Support common cross-cutting concerns via injectors (depends on
InjectionProxyFactory+ annotation workflows) - β Integrate with unified injection pipeline
- π Performance optimization for minimal-change scenarios
Phase 3.4: Integration and Error Handling¶
- β Complete integration with TD-010 bootstrap process
- β Enhance error handling with detailed injection error messages
- β Add comprehensive test coverage for all injection types (method wrapping, register_instance regression)
- π Performance testing and optimization
- β Documentation and examples for injection patterns (refresh after proxy utility implementation)
πΊοΈ 11. Remaining Work Plan¶
- β
Sample Custom Injectors: Delivered reference
TransactionalInjectorandCachingInjectorexamples undertests/resources/di/injection/reference_injectors.pyto demonstrate proxy workflows without expanding the production API surface. - β Performance Benchmarking & Optimisation: Superseded by TD-018 DI Benchmarking Suite; performance measurement now lives in the new benchmarking TD.
- π Annotation Discovery Integration (TD-012): Coordinate with TD-012 to add annotation-driven auto-registration and close the outstanding acceptance criterion.
- π Documentation & Rollout Notes: Capture usage guidance for the reference injectors and document recommended benchmarking tooling once metrics are in place.
π 12. Changelog¶
- 2025-10-02: Initial draft technical design created
- 2025-10-04: Refined based on TD-010 analysis and feedback
- Added technical clarifications for constructor injection and cycle detection integration
- Enhanced error handling with InjectionError exception hierarchy
- Added injector registry with priority parameters
- Added comprehensive Test Matrix section
- Added Implementation Phases with clear milestones
- Added Progress tracking section
- Updated examples to show annotation-based discovery and TD-010 integration
- 2025-10-04: Fixed critical design issues identified during implementation
- Fixed Autowired Usage: Corrected to use type annotation syntax only:
prop: Autowired[Type] - Fixed Injector Protocol: Changed from
inject(instance: Any, container: "DiContainer") -> Anytoinject(target: object, container: "DiContainer") -> objectfor better type safety - Updated Implementation: AutowiredInjector scans class annotations for
Autowired[T]types using annotation-based discovery - Updated Examples: All usage examples now show correct annotation-based syntax
- Terminology Consistency: Changed
instanceparameter totargetthroughout for clarity - 2025-10-04 (Later): Removed unnecessary compatibility code
- Simplified Autowired: Removed descriptor protocol implementation (
__init__,__set_name__,__get__,__set__) - now pure type annotation - Simplified AutowiredInjector: Removed fallback logic for descriptor-based syntax and
_get_service_type()helper method - Cleaned Tests: Removed tests for descriptor-based syntax that never existed in production
- Rationale: TD-011 is new functionality with no pre-existing code to maintain compatibility with
- 2025-10-06: Housekeeping pass
- Updated remaining work plan to focus on reference injectors, performance benchmarking, and TD-012 alignment
- Refreshed metadata and added documentation follow-up item for rollout guidance
- 2025-10-06 (Later): Added reference injectors
- Documented
TransactionalInjectorandCachingInjectorexamples (seetests/resources/di/injection/reference_injectors.py) - Kept examples out of production exports to avoid expanding the supported API surface
- Added unit coverage validating injector behaviour and container proxy unwrapping
π 13. Glossary¶
- Injection: Automatic provision of dependencies to services
- Constructor Injection: Resolving dependencies as constructor parameters for registered types
- Property Injection: Setting dependencies on object properties after creation using
Autowired[T] - Method Wrapping: Intercepting method calls for cross-cutting concerns using shared proxy strategy
- AOP: Aspect-Oriented Programming - paradigm for separating cross-cutting concerns
- Autowired: Type annotation marking dependencies for automatic injection
- Injector Registry: System for managing custom injectors with priority-based ordering
- InjectionError: Base exception for injection-specific failures (subclasses:
ConstructorInjectionError,PropertyInjectionError,MethodInjectionError,InjectorPipelineError) - Annotation Discovery: Process of discovering injection points via the forthcoming TD-012 annotation module