π Technical Design 012 β Enhanced DI Annotations¶
| Field | Value |
|---|---|
| ID | TD-012 |
| Title | Enhanced DI Annotations |
| Status | Complete |
| Owner | Contributors |
| Audience | Contributors, maintainers, DI system users |
| Created | 2025-10-02 |
| Last Updated | 2025-10-08 |
| Reviewers | TBD |
| Approvers | TBD |
| Tracking | Phase 4 of DI Roadmap |
| Related | 001-dependency-injection-roadmap.md |
π§ 1. Summary¶
This technical design implements Phase 4 of the DI roadmap by adding Spring Boot-style annotations for declarative service registration and lifecycle metadata. The design introduces @service, @component, and related lifecycle annotations, alongside @configuration/@bean metadata to coordinate with the TD-011 injector pipeline and prepare for Phase 5 scanning.
At a glance:
- Spring Boot-inspired annotation system for declarative service registration
- Service/component distinction with automatic lifecycle metadata
@configuration/@beanhooks that integrate with the container and TD-011 injectors- Annotation metadata structured for future module scanning (TD-013)
- Configuration binding concerns deferred to dedicated design (TD-019, see below)
π 2. Context and Problem Statement¶
The Phase 1-3 container provides powerful registration, bootstrapping, and injection but requires manual service registration:
Current Limitations¶
- Manual Registration - All services must be explicitly registered in code
- No Declarative Metadata - Service lifecycle and metadata not expressed in service classes
- No Configuration Integration - No automatic binding of configuration files to services
- No Service Discovery - No automatic discovery of services based on annotations
- Limited Metadata - No way to express service metadata for tooling and runtime behavior
2.1 Goals¶
- Declarative Annotations -
@service,@componentannotations for automatic registration metadata - Lifecycle Control - Lifecycle hints surfaced through annotation metadata
- Configuration Coordination -
@configurationclasses with@beanfactory methods that integrate with TD-011 injectors - Scanning Friendly Metadata - Annotation structures compatible with TD-013 discovery pipeline
- Extensible Annotation Model - Clear extension points for future decorators without breaking the container
2.2 Non-Goals (Out of Scope)¶
- Automatic module scanning (Phase 5)
- Configuration value binding (covered by TD-019)
- Annotation metadata inheritance from parent classes (future consideration)
- Complex conditional registration (future consideration)
- Validation integration (future consideration)
- Advanced AOP beyond Phase 3 method wrapping
2.3 Assumptions and Constraints¶
- Phase 1-3 DiContainer implementation is available
- Annotation system builds on existing
loryth.common.annotations - Configuration value binding is completely deferred to TD-019 (no
Config[...]types in this implementation) - Reflection system provides annotation discovery capabilities
- Protocol registration is limited to directly implemented protocols only (not inherited interfaces or ABC mixins)
π§© 3. Design Overview¶
The annotation system captures metadata for service classes and bean-producing configuration classes, feeding the DI container and the forthcoming scanning pipeline. Configuration value binding remains deferred to TD-019.
graph TD
ServiceAnnotation["@service/@component"] --> AnnotationCapture[Annotation Capture]
ConfigurationAnnotation["@configuration/@bean"] --> AnnotationCapture
SingletonAnnotation["@singleton"] --> AnnotationCapture
TransientAnnotation["@transient"] --> AnnotationCapture
AnnotationCapture --> AnnotationRegistry[AnnotationRegistry]
AnnotationRegistry --> InjectorBridge["InjectorBridge (TD-011)"]
AnnotationRegistry --> Scanner["Scanner Integration (TD-013)"]
classDef annotations fill:none,stroke:#0E86D4,stroke-width:2px,color:#0E86D4
classDef components fill:none,stroke:#C55FFC,stroke-width:2px,color:#C55FFC
classDef integartions fill:none,stroke:#1FB071,stroke-width:2px,color:#1FB071
class ServiceAnnotation,ConfigurationAnnotation,SingletonAnnotation,TransientAnnotation annotations
class AnnotationCapture,AnnotationRegistry components
class InjectorBridge,Scanner integartions 3.1 Components Overview¶
| Component | Purpose |
|---|---|
AnnotationBuilder | Normalises decorator input into consistent metadata objects |
AnnotationRegistry | Stores resolved metadata for reuse by TD-011 injectors and TD-013 scan |
InjectorBridge | Pushes registry entries into the DI containerβs injector pipeline |
BeanMetadata | Describes @bean factory methods discovered on configuration classes |
3.2 Planned Annotations¶
| Annotation | Applied To | Notes |
|---|---|---|
@service | Class | Declares singleton service by default (overridable lifecycle); auto-registers implemented protocols |
@component | Class | Marks DI participant requiring explicit lifecycle annotation |
@singleton | Class | Forces singleton lifecycle when combined with @component |
@transient | Class | Forces transient lifecycle when combined with @component |
@configuration | Class | Groups related bean factories and lifecycle metadata |
@bean | Method | Declares bean factory method; lifecycle metadata stored for TD-011 |
π 4. Implementation Details¶
4.1 Core Service Annotations¶
from dataclasses import dataclass
from typing import Callable, TypeVar
from loryth.common.annotations import Annotation, AnnotationContext
from loryth.common.di.lifecycle import ServiceLifecycle
T = TypeVar('T')
@dataclass(frozen=True)
class ServiceAnnotation(Annotation):
lifecycle: ServiceLifecycle
def __init__(self, lifecycle: ServiceLifecycle) -> None:
super().__init__(name="service")
object.__setattr__(self, "lifecycle", lifecycle)
def service(*, lifecycle: ServiceLifecycle = ServiceLifecycle.SINGLETON) -> Callable[[type[T]], type[T]]:
def decorator(cls: type[T]) -> type[T]:
AnnotationContext.annotate(cls, ServiceAnnotation(lifecycle=lifecycle))
return cls
return decorator
@service()
class ExampleService:
...
Other annotations (@component, @singleton, @transient, @configuration, @bean) reuse the same pattern: define an Annotation subclass to hold metadata and expose a thin decorator that calls AnnotationContext.annotate with the desired payload.
4.2 Annotation Metadata & Processor¶
The processor runs during DiContainerBuilder bootstrap today and will also execute once TD-013 supplies discovered classes. Its sole responsibility is to translate classes into registry entries and hand them off to the injector bridge; no container wiring occurs directly inside the processor. This keeps metadata generation reusable for future tooling (documentation, scanning diffs, IDE support).
from dataclasses import dataclass
from typing import Iterable
from loryth.common.di.lifecycle import ServiceLifecycle
@dataclass(frozen=True)
class BeanMetadata:
method_name: str
lifecycle: ServiceLifecycle
return_type: type
@dataclass(frozen=True)
class ClassAnnotationMetadata:
cls: type
lifecycle: ServiceLifecycle
beans: tuple[BeanMetadata, ...]
module: str
qualname: str
interfaces: tuple[type, ...]
class AnnotationProcessor:
def __init__(self, builder: AnnotationBuilder, registry: AnnotationRegistry, bridge: InjectorBridge) -> None:
self._builder = builder
self._registry = registry
self._bridge = bridge
def process(self, classes: Iterable[type]) -> None:
for cls in classes:
metadata = self._builder.build(cls)
if metadata is None:
continue
self._registry.add(metadata)
self._bridge.register(metadata)
- AnnotationBuilder encapsulates the decorator parsing demonstrated in Β§4.1 and raises
DiRegistrationErrorwhen conflicting lifecycle annotations are detected. - AnnotationRegistry persists metadata for later consumption (TD-013).
- InjectorBridge converts registry entries into container registrations by calling
container.register_singleton()/container.register_transient()with the appropriate service types and factory functions. - Baseline metadata includes the declaring module, fully-qualified name, and directly implemented protocols (not inherited interfaces); TD-013 can extend this set as needed.
- The processor is invoked via
DiContainerBuilder.process_annotated_classes([...]), allowing explicit registration before bootstrap.
Future TDs can extend the processor by swapping the classes iterable (e.g., feeding scanner results) without changing container internals.
4.3 Interface Sketches¶
from collections.abc import Iterable
from typing import Protocol
class AnnotationBuilder(Protocol):
"""Turns annotated classes into structured metadata; returns None when unannotated."""
def build(self, cls: type) -> ClassAnnotationMetadata | None: ...
class AnnotationRegistry(Protocol):
"""Stores class metadata for reuse by TD-011 injector bridge and TD-013 scanning."""
def add(self, metadata: ClassAnnotationMetadata) -> None: ...
def iter_metadata(self) -> Iterable[ClassAnnotationMetadata]: ...
class InjectorBridge(Protocol):
"""Registers annotated services/beans with the container without exposing registry internals."""
def register(self, metadata: ClassAnnotationMetadata) -> None: ...
The concrete implementations can add caching, metrics, or incremental update support later without changing the processor contract.
βοΈ 5. Semantics¶
5.1 Annotation Precedence¶
@serviceimplies singleton lifecycle unless overridden by explicit lifecycle annotations.@componentrequires explicit lifecycle metadata via@singletonor@transient.@configurationclasses are treated as metadata contributors; they do not participate in lifecycle semantics and defer value binding entirely to TD-019.- Annotated classes are automatically registered for each directly implemented protocol (using
Protocolas base); inherited interfaces and ABC mixins are not registered. - Annotation metadata inheritance from parent classes is deferred to future work (out of scope for this TD).
- Conflicting lifecycle annotations (e.g.,
@singleton+@transient) raiseDiRegistrationError, aDiErrorsubtype surfaced during metadata build. - Class metadata gathered here flows into the container via
InjectorBridgecallingcontainer.register_singleton()/register_transient()so annotated services respect the same constructor/property injection rules as manual registrations.
5.2 Registry Semantics¶
- Every annotated class produces a
ClassAnnotationMetadatarecord stored in the registry. - Registry entries capture lifecycle, bean metadata, and source module for TD-013 scanning reuse.
- The registry is append-only per processing run, enabling tooling to diff successive loads for developer feedback.
5.3 Bean Factory Semantics¶
- Bean methods are registered as factory functions via
container.register_singleton()/register_transient()with the appropriate lifecycle. - Bean return types must be explicitly annotated; missing annotations raise
DiRegistrationError. - Bean lifecycle metadata informs whether the container creates singleton or transient instances from the factory; caching is handled by the container.
- Bean factories are not invoked during annotation processingβthey are registered as standard factory providers and execute lazily when resolved (or eagerly during bootstrap for singletons).
- Failures during bean factory execution are wrapped in existing DI exception types (
DiResolutionError,SingletonInitializationError) following TD-011 conventions. - Bean metadata is captured for TD-013 scanning compatibility, but configuration value binding is entirely delegated to TD-019.
π§ͺ 6. Examples¶
6.1 Service Annotations¶
@service # Implies singleton lifecycle
class UserService:
def __init__(self, repository: UserRepository) -> None:
self.repository = repository
@component # Requires explicit lifecycle
@transient
class RequestHandler:
def __init__(self, audit_logger: AuditLogger) -> None:
self.audit_logger = audit_logger
def handle_request(self, request: Request) -> Response:
self.audit_logger.record(request)
return Response()
# Override default singleton behaviour
@service
@transient
class SessionManager:
pass
6.2 Configuration Classes and Bean Metadata¶
# Note: Configuration value binding (Config[...]) is completely deferred to TD-019
# This example shows only metadata capture and bean factory registration
@configuration
class MetricsConfiguration:
@bean
def reporter(self, transport: MetricsTransport) -> MetricsReporter:
# Transport is resolved from container automatically
return MetricsReporter(transport=transport)
@bean(lifecycle=ServiceLifecycle.TRANSIENT)
def buffered_reporter(self, reporter: MetricsReporter) -> BufferedMetricsReporter:
# Reporter is resolved from container automatically
return BufferedMetricsReporter(reporter)
6.3 Processing Annotated Classes¶
from loryth.common.di import DiContainerBuilder
# Using the builder pattern with new process_annotated_classes method
builder = DiContainerBuilder()
builder.process_annotated_classes([
UserService,
RequestHandler,
SessionManager,
MetricsConfiguration,
])
container = builder.build()
container.bootstrap()
# Annotated services are now registered and can be resolved
user_service = container.resolve(UserService)
π§ͺ 7. Test Organisation & Matrix¶
7.1 Test Organisation¶
Unit Tests Location¶
tests/unit/di/annotations/
βββ test_decorators.py # decorator metadata + protocol registration
βββ test_builder.py # annotation builder normalisation
βββ test_inheritance.py # subclass overrides using base + mixin graphs
Integration Tests Location¶
tests/integration/di/annotations/
βββ test_resolution_flow.py # successful bootstrap + resolution scenarios
βββ test_bean_failures.py # intentional bean failure wrapped in BeanError
βββ fixtures.py # success + failure container bootstrap helpers
7.2 Test Matrix¶
Unit Coverage¶
- β
Default Service Metadata:
@serviceproduces singleton lifecycle and protocol registrations - β
Explicit Lifecycle Overrides:
@component+@transient/@singleton - β
Bean Annotation Parsing:
@beanmethods capture name, lifecycle, return type - β
Lifecycle Conflict Handling:
@componentwith both@singletonand@transientraisesDiRegistrationError - β Protocol Detection: Only directly implemented protocols are registered (not inherited interfaces)
Fixture strategy: ad-hoc service classes defined inline per test. The canonical inheritance graph (BaseService β LoggingMixin β ConcreteService) is used to construct realistic test scenarios demonstrating protocol registration and lifecycle behavior.
Integration Coverage¶
- β Constructor Injection: Annotated services resolve and pull dependencies via TD-011
- β Property Injection: Autowired-style properties populate after resolution
- β Bean Factory Registration: Bean factories registered as factory providers, invoked lazily on resolution
- β Mixed Lifecycle Graphs: Combination of singleton/transient services plus configuration classes
- β Registry Persistence: Metadata captured in registry for TD-013 scanning compatibility
- β
Builder Integration:
DiContainerBuilder.process_annotated_classes()correctly registers annotated services
Fixture strategy: lightweight container bootstrap helpers using DiContainerBuilder.process_annotated_classes(). Canonical inheritance graph (BaseService β LoggingMixin β ConcreteService) used to build realistic dependency scenarios.
Testing notes: - Conflicting lifecycle annotations are expected to raise DiRegistrationError with a concise explanation. - Bean factory failures during resolution wrapped in existing DI exceptions (DiResolutionError, SingletonInitializationError).
β 8. Acceptance Criteria¶
-
@serviceannotation registers classes with singleton lifecycle by default, overridable via lifecycle annotations. -
@componentannotation enforces explicit lifecycle metadata with clear error reporting. -
@configurationclasses emit bean metadata captured in registry; configuration value binding completely deferred to TD-019. - Bean methods registered as factory providers via
InjectorBridgecallingcontainer.register_singleton()/register_transient(). - Bean return types must be explicitly annotated; missing annotations raise
DiRegistrationError. - Annotation registry captures class metadata (module, qualname, directly implemented protocols) for consumption by TD-013 scanning.
- Annotated services resolve through the same constructor/property injection rules as manual registrations.
- Conflicting lifecycle annotations surface
DiRegistrationErrorwith actionable messaging. -
DiContainerBuilder.process_annotated_classes([...])method provides explicit registration entry point.
π 9. Rollout and Migration¶
9.1 Implementation Phases¶
Phase 4a: Core annotation definitions - Implement @service, @component, @singleton, @transient decorators. - Introduce metadata dataclasses shared across container and tooling.
Phase 4b: Processor + Injector bridge - Build annotation processor and registry infrastructure. - Integrate with TD-011 injector pipeline so annotated services honour constructor/property injection.
Phase 4c: Scanning hand-off - Finalise registry contract required by TD-013 scanning. - Provide incremental processing strategy for tooling. - Coordinate with TD-019 for configuration injector interop.
9.2 Backward Compatibility¶
- Existing manual registration behaviour remains unchanged.
- Annotation processor is optional; projects can adopt incrementally.
- Configuration binding lives in TD-019, allowing alternative solutions without impacting this design.
β οΈ 10. Risks and Mitigations¶
10.1 Metadata Divergence Risk¶
Risk: Annotation metadata structures drift between the container, TD-013 scanning, and downstream tooling. Mitigation: Maintain shared dataclasses and add contract tests that compare registry output with scanning expectations.
10.2 Annotation Processing Performance Risk¶
Risk: Processing large codebases incurs noticeable latency. Mitigation: Cache reflection results, support incremental processing, and expose metrics for profiling.
10.3 Injector Bridge Coupling Risk¶
Risk: Changes in TD-011 injector behaviour break annotated service registration silently. Mitigation: Provide end-to-end tests that exercise annotated services through the injector pipeline and document the bridge contract.
π 11. Progress¶
Legend: [x] = complete, [ ] = pending
- Implement core service/lifecycle annotations (@service, @component, @singleton, @transient).
- Publish annotation metadata dataclasses and registry interfaces.
- Implement annotation processor and injector bridge integration.
- Add unit tests covering annotated service registration and bean metadata extraction.
- Add integration tests verifying TD-011 constructor/property injection for annotated services.
- Document registry consumption flow required by TD-013 scanning.
- Coordinate with TD-019 to validate
typing.Annotated+Config[...]interoperability.
π 12. Changelog¶
- 2025-10-08: TD-012 implementation completed. All acceptance criteria met, test suite passes (59 tests), status updated to Complete. Only remaining work is TD-019 coordination for configuration value binding.
- 2025-10-08: Implementation decisions clarified: (1) no configuration value binding, (2)
InjectorBridgecalls container registration methods, (3) bean methods registered as factories, (4) only direct protocols registered, (5) annotation inheritance out of scope, (6) builder integration viaprocess_annotated_classes(). - 2025-10-06: Scope refined; configuration binding extracted to TD-019 and registry/injector bridge clarified.
- 2025-10-02: Initial draft technical design created.
π 13. Glossary¶
- Annotation: Metadata attached to classes, methods, or properties for declarative configuration.
- Service: A class that provides functionality and can be injected as a dependency.
- Component: A generic class that participates in dependency injection and requires explicit lifecycle metadata.
- Configuration Class: A class that publishes bean factory methods and metadata for the container.
- Bean: A factory method within a configuration class that creates service instances.
- Config Injector: A
typing.Annotatedhelper defined in TD-019 that supplies configuration values to properties or parameters.