Skip to content

๐Ÿ“˜ Technical Design 015 โ€” DI System Modularization

Field Value
ID TD-015
Title DI System Modularization
Status Draft
Owner Contributors
Audience Contributors, maintainers, architecture reviewers
Created 2025-10-02
Last Updated 2025-10-02
Reviewers TBD
Approvers TBD
Tracking Phase 7 of DI Roadmap
Related 001-dependency-injection-roadmap.md

๐Ÿงญ 1. Summary

This technical design implements Phase 7 of the DI roadmap by extracting DI components into organized submodules for improved maintainability and readability. The design reorganizes the monolithic DI implementation into focused, cohesive modules while maintaining backward compatibility.

At a glance:

  • Extract DI components into logical submodules under loryth.common.di
  • Reorganize injection system, annotation processing, and scanning into separate modules
  • Improve file size management and reduce complexity per module
  • Maintain backward compatibility through preserved public API
  • Create clear module boundaries with well-defined responsibilities

๐Ÿ“š 2. Context and Problem Statement

The DI system has grown through Phases 1-6 into a comprehensive framework with potential maintainability challenges:

Current State Analysis

src/loryth/common/di/
โ”œโ”€โ”€ __init__.py              # Module exports (growing large)
โ”œโ”€โ”€ new_container.py         # Core container (500+ lines projected)
โ”œโ”€โ”€ exceptions.py            # DI-specific exceptions
โ”œโ”€โ”€ lifecycle.py             # Service lifecycle enum
โ”œโ”€โ”€ container_item.py        # Container item dataclass
โ”œโ”€โ”€ annotations.py           # DI annotations (Phase 4 - will be large)
โ”œโ”€โ”€ injection.py             # Injection system (Phase 3 - will be large)
โ”œโ”€โ”€ scanning.py              # Package scanning (Phase 5 - will be large)
โ””โ”€โ”€ (other growing components)

Projected Issues

  1. Large File Sizes - Individual modules may exceed maintainability thresholds
  2. Mixed Responsibilities - Single files handling multiple concerns
  3. Poor Module Cohesion - Related functionality scattered across files
  4. Navigation Difficulty - Developers struggle to find relevant code
  5. Testing Complexity - Large modules require extensive test coverage

2.1 Goals

  1. Logical Modularization - Organize code by functional responsibility
  2. File Size Management - Keep individual modules under ~500 lines
  3. Improved Cohesion - Group related functionality together
  4. Better Navigation - Clear module structure for developers
  5. Maintained Compatibility - Preserve existing public API

2.2 Non-Goals (Out of Scope)

  • Changing public API or breaking backward compatibility
  • Performance optimization (separate concern)
  • Adding new functionality (handled in other phases)
  • Changing fundamental architecture patterns

2.3 Assumptions and Constraints

  • All Phase 1-6 functionality exists and is implemented
  • Modularization should not impact runtime performance
  • Public API surface remains exactly the same
  • Import paths for public APIs remain unchanged

๐Ÿงฉ 3. Design Overview

The modularization extracts DI components into organized submodules with clear responsibilities and boundaries.

graph TB
    subgraph "๐ŸŽฏ Current Structure"
        A[new_container.py<br/>~500+ lines]
        B[annotations.py<br/>~400+ lines]
        C[injection.py<br/>~600+ lines]
        D[scanning.py<br/>~400+ lines]
    end

    subgraph "๐Ÿ“ฆ New Submodules"
        E[container/]
        F[annotations/]
        G[injection/]
        H[scanning/]
    end

    subgraph "๐Ÿ”ง Container Module"
        I[core.py<br/>Core container]
        J[bootstrap.py<br/>Bootstrap logic]
        K[registration.py<br/>Registration API]
        L[resolution.py<br/>Resolution API]
    end

    subgraph "๐Ÿ’‰ Injection Module"
        M[manager.py<br/>Injection coordination]
        N[constructor.py<br/>Constructor injection]
        O[property.py<br/>Property injection]
        P[method.py<br/>Method wrapping]
    end

    A --> I
    A --> J
    A --> K
    A --> L

    C --> M
    C --> N
    C --> O
    C --> P

    E --> I
    E --> J
    E --> K
    E --> L

    G --> M
    G --> N
    G --> O
    G --> P

    classDef currentNode fill: none, stroke: #f44336, stroke-width: 2px, color: #ffffff
    classDef newNode fill: none, stroke: #4caf50, stroke-width: 2px, color: #ffffff
    classDef submoduleNode fill: none, stroke: #2196f3, stroke-width: 2px, color: #ffffff

    class A,B,C,D currentNode
    class E,F,G,H newNode
    class I,J,K,L,M,N,O,P submoduleNode

3.1 Target Module Structure

src/loryth/common/di/
โ”œโ”€โ”€ __init__.py                    # Public API exports (unchanged)
โ”œโ”€โ”€ exceptions.py                  # DI exceptions (unchanged)
โ”œโ”€โ”€ lifecycle.py                   # Service lifecycle (unchanged)
โ”œโ”€โ”€ container_item.py              # Container item (unchanged)
โ”œโ”€โ”€ container/
โ”‚   โ”œโ”€โ”€ __init__.py               # Container submodule exports
โ”‚   โ”œโ”€โ”€ core.py                   # Core DiContainer class
โ”‚   โ”œโ”€โ”€ bootstrap.py              # Bootstrap and cycle detection
โ”‚   โ”œโ”€โ”€ registration.py           # Registration method implementations
โ”‚   โ””โ”€โ”€ resolution.py             # Resolution method implementations
โ”œโ”€โ”€ injection/
โ”‚   โ”œโ”€โ”€ __init__.py               # Injection submodule exports
โ”‚   โ”œโ”€โ”€ manager.py                # InjectionManager coordination
โ”‚   โ”œโ”€โ”€ constructor.py            # Constructor injection logic
โ”‚   โ”œโ”€โ”€ property.py               # Property injection with Autowired
โ”‚   โ”œโ”€โ”€ method.py                 # Method wrapping and AOP
โ”‚   โ””โ”€โ”€ descriptors.py            # Autowired and other descriptors
โ”œโ”€โ”€ annotations/
โ”‚   โ”œโ”€โ”€ __init__.py               # Annotation submodule exports
โ”‚   โ”œโ”€โ”€ service.py                # Service and component annotations
โ”‚   โ”œโ”€โ”€ configuration.py          # Configuration and bean annotations
โ”‚   โ”œโ”€โ”€ processor.py              # Annotation processor
โ”‚   โ””โ”€โ”€ config_binding.py         # Configuration value binding
โ”œโ”€โ”€ scanning/
โ”‚   โ”œโ”€โ”€ __init__.py               # Scanning submodule exports
โ”‚   โ”œโ”€โ”€ package_scanner.py        # Package and module scanning
โ”‚   โ”œโ”€โ”€ filters.py                # Annotation and pattern filters
โ”‚   โ”œโ”€โ”€ application.py            # Application bootstrap
โ”‚   โ””โ”€โ”€ discovery.py              # Class discovery utilities
โ””โ”€โ”€ configuration/
    โ”œโ”€โ”€ __init__.py               # Configuration submodule exports
    โ”œโ”€โ”€ providers.py              # Config providers (YAML, JSON, TOML)
    โ”œโ”€โ”€ loader.py                 # Configuration loading
    โ””โ”€โ”€ binder.py                 # Configuration value binding

๐Ÿ“ 4. Implementation Details

4.1 Container Submodule Organization

# src/loryth/common/di/container/__init__.py
"""Container submodule with core DI functionality."""

from .core import DiContainer
from .bootstrap import BootstrapManager, DependencyCycleDetector
from .registration import RegistrationMixin
from .resolution import ResolutionMixin

__all__ = [
    'DiContainer',
    'BootstrapManager',
    'DependencyCycleDetector',
    'RegistrationMixin',
    'ResolutionMixin'
]

# src/loryth/common/di/container/core.py
"""Core DiContainer class with composition of mixins."""

from typing import Any, TypeVar
from ..lifecycle import ServiceLifecycle
from ..container_item import DiContainerItem
from .registration import RegistrationMixin
from .resolution import ResolutionMixin
from .bootstrap import BootstrapManager

T = TypeVar('T')

class DiContainer(RegistrationMixin, ResolutionMixin):
    """
    Enhanced dependency injection container with lifecycle management.

    Combines registration, resolution, and bootstrap capabilities through
    composition of focused mixins.
    """

    def __init__(self) -> None:
        super().__init__()
        self._items: list[DiContainerItem[Any]] = []
        self._cache: dict[type, Any] = {}
        self._dependencies: dict[type, list[type]] = {}
        self._bootstrap_manager = BootstrapManager(self)
        self._is_bootstrapped = False

    def bootstrap(self) -> None:
        """Trigger container bootstrap process."""
        if not self._is_bootstrapped:
            self._bootstrap_manager.bootstrap()
            self._is_bootstrapped = True

    def clear(self) -> None:
        """Clear all registrations and cache."""
        self._items.clear()
        self._cache.clear()
        self._dependencies.clear()
        self._is_bootstrapped = False

# src/loryth/common/di/container/registration.py
"""Registration method implementations."""

from typing import Any, Callable, TypeVar
from ..lifecycle import ServiceLifecycle
from ..container_item import DiContainerItem
from ..exceptions import DiRegistrationError

T = TypeVar('T')

class RegistrationMixin:
    """Mixin providing registration methods for DiContainer."""

    def register_singleton[T](self, service_type: type[T], implementation_type: type[T]) -> None:
        """Register a service as singleton."""
        self._validate_registration(service_type, implementation_type)
        self._analyze_and_track_dependencies(service_type, implementation_type)

        provider = self._create_singleton_provider(implementation_type)
        item = DiContainerItem(service_type, provider, ServiceLifecycle.SINGLETON)
        self._items.append(item)

    def register_transient[T](self, service_type: type[T], implementation_type: type[T]) -> None:
        """Register a service as transient."""
        self._validate_registration(service_type, implementation_type)
        self._analyze_and_track_dependencies(service_type, implementation_type)

        provider = self._create_transient_provider(implementation_type)
        item = DiContainerItem(service_type, provider, ServiceLifecycle.TRANSIENT)
        self._items.append(item)

    # Additional registration methods...

    def _validate_registration(self, service_type: type, implementation_type: type) -> None:
        """Validate registration compatibility."""
        # Implementation moved from main container
        pass

    def _create_singleton_provider(self, implementation_type: type) -> Callable:
        """Create provider for singleton services."""
        # Implementation moved from main container
        pass

4.2 Injection Submodule Organization

# src/loryth/common/di/injection/__init__.py
"""Injection submodule with automatic dependency injection."""

from .manager import InjectionManager
from .constructor import ConstructorInjector
from .property import PropertyInjector, Autowired
from .method import MethodWrapper
from .descriptors import AutowiredDescriptor

__all__ = [
    'InjectionManager',
    'ConstructorInjector',
    'PropertyInjector',
    'Autowired',
    'MethodWrapper',
    'AutowiredDescriptor'
]

# src/loryth/common/di/injection/manager.py
"""Injection manager coordinating all injection phases."""

from typing import TypeVar, Any
from ..container_item import DiContainerItem
from .constructor import ConstructorInjector
from .property import PropertyInjector
from .method import MethodWrapper

T = TypeVar('T')

class InjectionManager:
    """Coordinates all phases of dependency injection."""

    def __init__(self, container) -> None:
        self._container = container
        self._constructor_injector = ConstructorInjector(container)
        self._property_injector = PropertyInjector(container)
        self._method_wrapper = MethodWrapper(container)

    def create_with_injection[T](self, item: DiContainerItem[T]) -> T:
        """Execute full injection pipeline."""
        # Phase 1: Constructor injection
        if self._should_use_constructor_injection(item):
            instance = self._constructor_injector.create_with_injection(item)
        else:
            instance = item.provider(self._container)

        # Phase 2: Property injection
        self._property_injector.inject_properties(instance)

        # Phase 3: Method wrapping
        wrapped_instance = self._method_wrapper.wrap_methods(instance)

        return wrapped_instance

# src/loryth/common/di/injection/property.py
"""Property injection implementation."""

from typing import TypeVar, Generic, Any
from ..exceptions import InjectionError

T = TypeVar('T')

class Autowired(Generic[T]):
    """Property descriptor for automatic dependency injection."""

    def __init__(self, service_type: type[T] | None = None) -> None:
        self._service_type = service_type
        self._attr_name: str | None = None

    # Implementation moved from main injection module

class PropertyInjector:
    """Handles property injection for Autowired properties."""

    def __init__(self, container) -> None:
        self._container = container

    def inject_properties(self, instance: Any) -> None:
        """Inject dependencies into Autowired properties."""
        # Implementation moved from main injection module

4.3 Backward Compatibility Layer

# src/loryth/common/di/__init__.py
"""DI system public API - maintains backward compatibility."""

# Core container (unchanged public API)
from .container import DiContainer

# Lifecycle and items (unchanged)
from .lifecycle import ServiceLifecycle
from .container_item import DiContainerItem

# Exceptions (unchanged)
from .exceptions import (
    DiError,
    DependencyNotFoundError,
    DependencyCycleError,
    DiRegistrationError,
    DiResolutionError
)

# Injection system (maintain public imports)
from .injection import (
    InjectionManager,
    ConstructorInjector,
    PropertyInjector,
    Autowired,
    MethodWrapper
)

# Annotations (maintain public imports)
from .annotations import (
    Service, Component, Singleton, Transient,
    Configuration, Bean, ConfigValue,
    service, component, singleton, transient,
    configuration, bean
)

# Scanning (maintain public imports)
from .scanning import (
    ApplicationAnnotation, Application,
    PackageScanner, ScanningConfiguration,
    ApplicationBootstrap,
    application
)

# Configuration (maintain public imports)
from .configuration import (
    ConfigProvider,
    YamlConfigProvider,
    JsonConfigProvider,
    TomlConfigProvider,
    ConfigurationLoader,
    ConfigurationBinder
)

# Preserve all existing public API exports
__all__ = [
    # Core
    'DiContainer',
    'ServiceLifecycle',
    'DiContainerItem',

    # Exceptions
    'DiError',
    'DependencyNotFoundError',
    'DependencyCycleError',
    'DiRegistrationError',
    'DiResolutionError',

    # Injection
    'InjectionManager',
    'ConstructorInjector',
    'PropertyInjector',
    'Autowired',
    'MethodWrapper',

    # Annotations
    'Service', 'Component', 'Singleton', 'Transient',
    'Configuration', 'Bean', 'ConfigValue',
    'service', 'component', 'singleton', 'transient',
    'configuration', 'bean',

    # Scanning
    'Application', 'PackageScanner', 'ScanningConfiguration',
    'ApplicationBootstrap', 'application',

    # Configuration
    'ConfigProvider', 'YamlConfigProvider', 'JsonConfigProvider',
    'TomlConfigProvider', 'ConfigurationLoader', 'ConfigurationBinder'
]

4.4 Migration Strategy

class ModularizationMigration:
    """Manages migration to modularized structure."""

    EXTRACTION_PLAN = {
        # Container extraction
        "new_container.py": {
            "DiContainer.__init__": "container/core.py",
            "DiContainer.register_*": "container/registration.py",
            "DiContainer.resolve*": "container/resolution.py",
            "DiContainer.bootstrap": "container/bootstrap.py",
            "BootstrapManager": "container/bootstrap.py",
            "DependencyCycleDetector": "container/bootstrap.py"
        },

        # Injection extraction
        "injection.py": {
            "InjectionManager": "injection/manager.py",
            "ConstructorInjector": "injection/constructor.py",
            "PropertyInjector": "injection/property.py",
            "Autowired": "injection/property.py",
            "MethodWrapper": "injection/method.py"
        },

        # Annotations extraction
        "annotations.py": {
            "Service, Component, etc.": "annotations/service.py",
            "Configuration, Bean, etc.": "annotations/configuration.py",
            "AnnotationProcessor": "annotations/processor.py",
            "ConfigurationBinder": "annotations/config_binding.py"
        },

        # Scanning extraction
        "scanning.py": {
            "PackageScanner": "scanning/package_scanner.py",
            "ScanningFilter": "scanning/filters.py",
            "ApplicationBootstrap": "scanning/application.py",
            "ModuleLoader": "scanning/discovery.py"
        }
    }

    def validate_extraction(self, source_file: str, target_module: str) -> bool:
        """Validate that extraction maintains functionality."""
        # Check that all dependencies are preserved
        # Verify that public API remains unchanged
        # Ensure no circular dependencies introduced
        pass

โš™๏ธ 5. Semantics

5.1 Module Responsibilities

  • container/: Core container functionality, registration, resolution, bootstrap
  • injection/: All forms of dependency injection (constructor, property, method)
  • annotations/: DI annotation system and processing
  • scanning/: Package scanning and application bootstrap
  • configuration/: Configuration loading and binding

5.2 Import Rules

  • Public API Unchanged: All existing imports continue to work
  • Internal Reorganization: Internal module structure is implementation detail
  • Submodule Access: Internal code can import from submodules directly
  • No Circular Dependencies: Submodules must have clear dependency hierarchy

5.3 File Size Guidelines

  • Target Size: Individual modules should be under 500 lines
  • Logical Cohesion: Related functionality grouped together
  • Single Responsibility: Each module has clear, focused purpose

๐Ÿงช 6. Examples

6.1 Unchanged Public API

# All existing imports continue to work exactly the same
from loryth.common.di import Container, ServiceLifecycle
from loryth.common.di import Autowired, service, component
from loryth.common.di import Application, application

# Usage remains identical
container = Container()
container.register_singleton(UserService, UserService)


@service
class EmailService:
    user_service: Annotated[UserService, Autowired()]


@application(packages=["myapp.services"])
class MyApp:
    pass

6.2 Internal Module Organization

# Internal imports use new modular structure
# (These are implementation details, not public API)

# Container module
from loryth.common.di.container import Container, BootstrapManager
from loryth.common.di.container.registration import RegistrationMixin
from loryth.common.di.container.resolution import ResolutionMixin

# Injection module
from loryth.common.di.injection import InjectionManager
from loryth.common.di.injection.constructor import ConstructorInjector
from loryth.common.di.injection.property import PropertyInjector, Autowired

# Annotations module
from loryth.common.di.annotations.service import Service, Component
from loryth.common.di.annotations.processor import AnnotationProcessor

6.3 Development Benefits

# Developers can navigate to focused modules
# Instead of searching through 1000+ line files

# Find registration logic
# -> container/registration.py (~150 lines)

# Find property injection logic
# -> injection/property.py (~200 lines)

# Find annotation processing logic
# -> annotations/processor.py (~250 lines)

# Each module has single, clear responsibility

โœ… 7. Acceptance Criteria

  • All DI functionality is extracted into logical submodules
  • Individual module files are under 500 lines each
  • Public API remains completely unchanged (100% backward compatible)
  • All existing imports continue to work without modification
  • Module responsibilities are clearly defined and documented
  • No circular dependencies exist between submodules
  • Test suites continue to pass without modification
  • Code navigation and maintainability is improved
  • Module structure supports future extensibility
  • Internal imports are clean and well-organized

โ“ 8. Open Questions

8.1 Modularization Strategy Questions

  1. Granularity Balance: How should we balance module granularity:
  2. Should we create many small modules or fewer larger ones?
  3. Should related functionality be grouped even if it creates larger modules?
  4. Should we optimize for discoverability or file size?

  5. Responsibility Boundaries: How should we define module boundaries:

  6. Should modules be organized by feature or by architectural layer?
  7. Should cross-cutting concerns be in separate modules or distributed?
  8. Should we allow some overlap between module responsibilities?

  9. Migration Strategy: How should we approach the modularization migration:

  10. Should we modularize incrementally or all at once?
  11. Should we maintain both old and new structures temporarily?
  12. Should we provide migration utilities for internal code?

8.2 Architecture and Design Questions

  1. Dependency Management: How should submodules handle dependencies:
  2. Should submodules import from each other or only from parent?
  3. Should we enforce strict dependency hierarchies?
  4. Should we allow bidirectional dependencies for closely related modules?

  5. Code Duplication: How should we handle shared functionality:

  6. Should common utilities be in a shared module or duplicated?
  7. Should we create base classes to avoid duplication?
  8. Should we prefer composition or inheritance for shared behavior?

  9. Testing Strategy: How should testing adapt to modularized structure:

  10. Should tests follow the same module structure?
  11. Should we test modules in isolation or integration?
  12. Should module boundaries influence test organization?

8.3 Performance and Compatibility Questions

  1. Import Performance: How should modularization affect import performance:
  2. Should we optimize for lazy loading or eager loading?
  3. Should submodule imports be cached or computed on demand?
  4. Should we measure and monitor import performance impact?

  5. Backward Compatibility: How strictly should we maintain compatibility:

  6. Should internal implementation details remain stable?
  7. Should we support undocumented import paths that may exist?
  8. Should we provide migration warnings for internal imports?

8.4 Development Experience Questions

  1. IDE Support: How should modularization improve development experience:
  2. Should module structure optimize for IDE navigation?
  3. Should we provide IDE-specific organization hints?
  4. Should module names support autocomplete and discovery?

  5. Documentation: How should modularization be documented:

    • Should we document internal module structure?
    • Should we provide architectural diagrams for module relationships?
    • Should module documentation include usage guidelines?

8.5 Future Extensibility Questions

  1. Extension Points: How should modularization support future extensions:

    • Should module structure accommodate unknown future features?
    • Should we provide plugin interfaces at module boundaries?
    • Should modules be designed for independent versioning?
  2. Maintenance: How should modularized structure affect maintenance:

    • Should modules have dedicated maintainers or teams?
    • Should module changes require cross-module compatibility testing?
    • Should we provide tooling for cross-module refactoring?
  3. Evolution: How should the modular structure evolve over time:

    • Should module boundaries be stable or allow reorganization?
    • Should we version the module structure separately from functionality?
    • Should module evolution be coordinated with public API changes?

๐Ÿš€ 9. Rollout and Migration

9.1 Implementation Phases

Phase 7a: Design and planning - Finalize module structure and responsibilities - Create extraction mapping from current files to new modules - Design backward compatibility preservation strategy

Phase 7b: Submodule creation - Create new submodule directory structure - Extract code into appropriate modules - Implement backward compatibility imports

Phase 7c: Validation and cleanup - Validate that all functionality is preserved - Ensure test suites continue to pass - Clean up any temporary compatibility code

9.2 Migration Timeline

Week 1: Design and Planning
- Finalize module structure
- Create detailed extraction plan
- Set up module scaffolding

Week 2: Container and Injection Modules
- Extract container functionality
- Extract injection functionality
- Validate core functionality

Week 3: Annotations and Scanning Modules
- Extract annotation system
- Extract scanning functionality
- Validate complete system

Week 4: Testing and Cleanup
- Comprehensive testing
- Performance validation
- Documentation updates

โš ๏ธ 10. Risks and Mitigations

10.1 Backward Compatibility Risk

Risk: Modularization breaks existing code that depends on internal imports Mitigation: Comprehensive compatibility testing, maintain public API exactly

10.2 Performance Impact Risk

Risk: Additional import overhead from modular structure Mitigation: Performance testing, optimize critical import paths

10.3 Circular Dependency Risk

Risk: Poor module organization creates circular dependencies Mitigation: Clear dependency hierarchy design, validation tooling

๐Ÿ“ˆ 11. Progress

Legend: [x] = complete, [ ] = pending

  • Design final module structure and responsibilities
  • Create extraction mapping for current code
  • Implement container submodule with core functionality
  • Implement injection submodule with injection logic
  • Implement annotations submodule with annotation processing
  • Implement scanning submodule with package scanning
  • Implement configuration submodule with config loading
  • Create backward compatibility layer in main init.py
  • Validate that all existing functionality is preserved
  • Update internal imports to use new module structure
  • Performance testing for modularized imports
  • Documentation of new module structure

๐Ÿ“ 12. Changelog

  • 2025-10-02: Initial draft technical design created

๐Ÿ“– 13. Glossary

  • Modularization: Process of organizing code into logical, cohesive modules
  • Submodule: Module within a larger module that handles specific functionality
  • Backward Compatibility: Ensuring existing code continues to work unchanged
  • Code Extraction: Moving code from one module to another while preserving functionality
  • Module Responsibility: The specific functionality and concerns handled by a module