Skip to content

πŸ“˜ 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/@bean hooks 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

  1. Manual Registration - All services must be explicitly registered in code
  2. No Declarative Metadata - Service lifecycle and metadata not expressed in service classes
  3. No Configuration Integration - No automatic binding of configuration files to services
  4. No Service Discovery - No automatic discovery of services based on annotations
  5. Limited Metadata - No way to express service metadata for tooling and runtime behavior

2.1 Goals

  1. Declarative Annotations - @service, @component annotations for automatic registration metadata
  2. Lifecycle Control - Lifecycle hints surfaced through annotation metadata
  3. Configuration Coordination - @configuration classes with @bean factory methods that integrate with TD-011 injectors
  4. Scanning Friendly Metadata - Annotation structures compatible with TD-013 discovery pipeline
  5. 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 DiRegistrationError when 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

  • @service implies singleton lifecycle unless overridden by explicit lifecycle annotations.
  • @component requires explicit lifecycle metadata via @singleton or @transient.
  • @configuration classes 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 Protocol as 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) raise DiRegistrationError, a DiError subtype surfaced during metadata build.
  • Class metadata gathered here flows into the container via InjectorBridge calling container.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 ClassAnnotationMetadata record 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: @service produces singleton lifecycle and protocol registrations
  • βœ… Explicit Lifecycle Overrides: @component + @transient/@singleton
  • βœ… Bean Annotation Parsing: @bean methods capture name, lifecycle, return type
  • βœ… Lifecycle Conflict Handling: @component with both @singleton and @transient raises DiRegistrationError
  • βœ… 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

  • @service annotation registers classes with singleton lifecycle by default, overridable via lifecycle annotations.
  • @component annotation enforces explicit lifecycle metadata with clear error reporting.
  • @configuration classes emit bean metadata captured in registry; configuration value binding completely deferred to TD-019.
  • Bean methods registered as factory providers via InjectorBridge calling container.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 DiRegistrationError with 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) InjectorBridge calls container registration methods, (3) bean methods registered as factories, (4) only direct protocols registered, (5) annotation inheritance out of scope, (6) builder integration via process_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.Annotated helper defined in TD-019 that supplies configuration values to properties or parameters.