Skip to content

Dependency Injection Implementation

This document provides the comprehensive technical reference for the dependency injection system implemented in the Open Host Factory Plugin, including container management, service registration, dependency resolution patterns, and Clean Architecture compliance.

Dependency Injection Overview

The plugin implements a comprehensive dependency injection (DI) system that:

  • Manages object lifecycles: Singleton and transient instances
  • Resolves dependencies automatically: Constructor injection with automatic resolution
  • Supports interface segregation: Depend on abstractions, not implementations
  • Enables testability: Easy mocking and testing through interface injection

DI Container Implementation

Core Container

The DI container is implemented in src/infrastructure/di/container.py:

class DIContainer:
    """Comprehensive dependency injection container."""

    def __init__(self):
        self._singletons: Dict[Type, Any] = {}
        self._factories: Dict[Type, Callable] = {}
        self._transients: Dict[Type, Type] = {}
        self._instances: Dict[Type, Any] = {}

    def register_singleton(self, interface: Type, implementation: Optional[Type] = None):
        """Register a singleton service."""
        if implementation is None:
            implementation = interface

        self._singletons[interface] = implementation

    def register_factory(self, interface: Type, factory: Callable):
        """Register a factory function."""
        self._factories[interface] = factory

    def register_transient(self, interface: Type, implementation: Type):
        """Register a transient service."""
        self._transients[interface] = implementation

    def get(self, interface: Type) -> Any:
        """Resolve a dependency."""
        # Check for existing instance
        if interface in self._instances:
            return self._instances[interface]

        # Check for factory
        if interface in self._factories:
            instance = self._factories[interface](self)
            return instance

        # Check for singleton
        if interface in self._singletons:
            implementation = self._singletons[interface]
            instance = self._create_instance(implementation)
            self._instances[interface] = instance
            return instance

        # Check for transient
        if interface in self._transients:
            implementation = self._transients[interface]
            return self._create_instance(implementation)

        # Try to create directly if it has @injectable decorator
        if hasattr(interface, '__injectable__'):
            return self._create_instance(interface)

        raise DependencyNotFoundError(f"No registration found for {interface}")

    def _create_instance(self, implementation: Type) -> Any:
        """Create instance with dependency injection."""
        # Get constructor signature
        signature = inspect.signature(implementation.__init__)
        parameters = signature.parameters

        # Resolve constructor dependencies
        kwargs = {}
        for param_name, param in parameters.items():
            if param_name == 'self':
                continue

            param_type = param.annotation
            if param_type != inspect.Parameter.empty:
                kwargs[param_name] = self.get(param_type)

        return implementation(**kwargs)

Injectable Decorator

The @injectable decorator marks classes for dependency injection:

# src/domain/base/dependency_injection.py
def injectable(cls):
    """Decorator to mark classes as injectable."""
    cls.__injectable__ = True
    return cls

# Usage example
@injectable
class ApplicationService:
    def __init__(self, 
                 command_bus: CommandBus,
                 query_bus: QueryBus,
                 logger: LoggingPort):
        self._command_bus = command_bus
        self._query_bus = query_bus
        self._logger = logger

Service Registration

Services are registered in dedicated modules within src/infrastructure/di/ following a specific dependency order to ensure correct initialization.

Service Registration Orchestration

The service registration process is orchestrated by src/infrastructure/di/services.py with the following dependency-aware order:

def register_all_services(container: Optional[DIContainer] = None) -> DIContainer:
    """Register all services in dependency order."""

    # 1. Register scheduler strategies first (needed by port adapters)
    from src.infrastructure.scheduler.registration import register_all_scheduler_types
    register_all_scheduler_types()

    # 2. Register core services (includes SchedulerPort registration)
    register_core_services(container)

    # 3. Register port adapters (uses SchedulerPort from core services)
    from src.infrastructure.di.port_registrations import register_port_adapters
    register_port_adapters(container)

    # 4. Setup CQRS infrastructure (handlers and buses)
    from src.infrastructure.di.container import _setup_cqrs_infrastructure
    _setup_cqrs_infrastructure(container)

    # 5. Register provider services (needed by infrastructure services)
    register_provider_services(container)

    # 6. Register infrastructure services
    register_infrastructure_services(container)

    # 7. Register server services (conditionally based on config)
    register_server_services(container)

    return container

This registration order ensures that: - Scheduler strategies are available before port adapters need them - Core services (including SchedulerPort) are registered before dependent services - Port adapters can properly resolve their dependencies from core services - CQRS infrastructure is set up with all necessary dependencies available - Provider and infrastructure services have access to all foundational components - Server services are registered last as they depend on all other layers

Registration Dependencies

The dependency graph for service registration follows this pattern:

Scheduler Strategies (Registry)
    ↓
Core Services (SchedulerPort, LoggingPort, ConfigurationPort)
    ↓
Port Adapters (Uses SchedulerPort from core services)
    ↓
CQRS Infrastructure (CommandBus, QueryBus, Handler Discovery)
    ↓
Provider Services (AWS, Strategy Factories)
    ↓
Infrastructure Services (Repositories, Templates)
    ↓
Server Services (FastAPI, REST API Handlers)

Core Services Registration

# src/infrastructure/di/core_services.py
def register_core_services(container: DIContainer) -> None:
    """Register core application services."""

    # Register logging port
    container.register_singleton(
        LoggingPort,
        lambda c: LoggingAdapter()
    )

    # Register configuration port
    container.register_singleton(
        ConfigurationPort,
        lambda c: ConfigurationAdapter(get_config_manager())
    )

    # Register error handling port
    container.register_singleton(
        ErrorHandlingPort,
        lambda c: ErrorHandlingAdapter(c.get(LoggingPort))
    )

    # Register container port
    container.register_singleton(
        ContainerPort,
        lambda c: ContainerAdapter(c)
    )

    # Register CQRS buses
    container.register_singleton(
        CommandBus,
        lambda c: CommandBus(
            logger=c.get(LoggingPort),
            event_publisher=c.get(EventPublisherPort)
        )
    )

    container.register_singleton(
        QueryBus,
        lambda c: QueryBus(logger=c.get(LoggingPort))
    )

    # Register application service
    container.register_singleton(ApplicationService)

Provider Services Registration

# src/infrastructure/di/provider_services.py
def register_provider_services(container: DIContainer) -> None:
    """Register provider-specific services."""

    # Register provider strategy factory
    container.register_factory(ProviderStrategyFactory, create_provider_strategy_factory)

    # Register provider context
    container.register_factory(ProviderContext, create_configured_provider_context)

    # Register AWS services if AWS provider is configured
    config_manager = container.get(ConfigurationManager)
    if _is_aws_provider_configured(config_manager):
        _register_aws_services(container)

def _register_aws_services(container: DIContainer) -> None:
    """Register AWS-specific services."""

    # Register AWS client
    container.register_singleton(AWSClient)

    # Register AWS operations
    container.register_singleton(AWSOperations)

    # Register AWS handler factory
    container.register_singleton(AWSHandlerFactory)

    # Register AWS adapters
    container.register_singleton(AWSTemplateAdapter)
    container.register_singleton(AWSMachineAdapter)
    container.register_singleton(AWSProvisioningAdapter)
    container.register_singleton(AWSRequestAdapter)
    container.register_singleton(AWSResourceManagerAdapter)

    # Register AWS provider strategy and adapter
    container.register_singleton(AWSProviderAdapter)
    container.register_singleton(AWSProviderStrategy)

    # Register AWS managers
    container.register_singleton(AWSInstanceManager)
    container.register_singleton(AWSResourceManagerImpl)

Command and Query Handler Registration

# src/infrastructure/di/command_handler_services.py
def register_command_handlers(container: DIContainer) -> None:
    """Register command handlers."""

    # Template command handlers
    container.register_singleton(ValidateTemplateHandler)
    container.register_singleton(ConvertTemplateHandler)

    # Request command handlers
    container.register_singleton(CreateRequestHandler)
    container.register_singleton(UpdateRequestStatusHandler)
    container.register_singleton(CompleteRequestHandler)

    # Machine command handlers
    container.register_singleton(ConvertMachineStatusCommandHandler)
    container.register_singleton(ConvertBatchMachineStatusCommandHandler)
    container.register_singleton(UpdateMachineStatusCommandHandler)
    container.register_singleton(CleanupMachineResourcesCommandHandler)

# src/infrastructure/di/query_handler_services.py
def register_query_handlers(container: DIContainer) -> None:
    """Register query handlers."""

    # Template query handlers
    container.register_singleton(GetTemplatesHandler)
    container.register_singleton(GetTemplateByIdHandler)

    # Request query handlers
    container.register_singleton(GetRequestsHandler)
    container.register_singleton(GetRequestByIdHandler)
    container.register_singleton(GetRequestStatusHandler)

    # Machine query handlers
    container.register_singleton(GetMachinesHandler)
    container.register_singleton(GetMachinesByRequestHandler)

    # System query handlers
    container.register_singleton(GetProviderConfigHandler)
    container.register_singleton(ValidateProviderConfigHandler)

Port and Adapter Pattern

The DI system supports the ports and adapters pattern through interface segregation:

Port Definitions

# src/domain/base/ports/logging_port.py
class LoggingPort(ABC):
    """Abstract logging interface."""

    @abstractmethod
    def info(self, message: str) -> None:
        pass

    @abstractmethod
    def error(self, message: str) -> None:
        pass

    @abstractmethod
    def warning(self, message: str) -> None:
        pass

# src/domain/base/ports/configuration_port.py
class ConfigurationPort(ABC):
    """Abstract configuration interface."""

    @abstractmethod
    def get(self, key: str, default: Any = None) -> Any:
        pass

    @abstractmethod
    def get_section(self, section: str) -> Dict[str, Any]:
        pass

Adapter Implementations

# src/infrastructure/adapters/logging_adapter.py
class LoggingAdapter(LoggingPort):
    """Concrete logging implementation."""

    def __init__(self):
        self._logger = get_logger(__name__)

    def info(self, message: str) -> None:
        self._logger.info(message)

    def error(self, message: str) -> None:
        self._logger.error(message)

    def warning(self, message: str) -> None:
        self._logger.warning(message)

# src/infrastructure/adapters/configuration_adapter.py
class ConfigurationAdapter(ConfigurationPort):
    """Concrete configuration implementation."""

    def __init__(self, config_manager: ConfigurationManager):
        self._config_manager = config_manager

    def get(self, key: str, default: Any = None) -> Any:
        return self._config_manager.get(key, default)

    def get_section(self, section: str) -> Dict[str, Any]:
        return self._config_manager.get_section(section)

Factory Pattern Integration

The DI container supports factory patterns for complex object creation:

Provider Strategy Factory

# src/infrastructure/factories/provider_strategy_factory.py
class ProviderStrategyFactory:
    """Factory for creating provider strategies."""

    def __init__(self, 
                 config_manager: ConfigurationPort,
                 logger: LoggingPort):
        self._config_manager = config_manager
        self._logger = logger

    def create_strategy(self, provider_type: str) -> ProviderStrategy:
        """Create provider strategy based on type."""
        if provider_type == "aws":
            return self._create_aws_strategy()
        else:
            raise UnsupportedProviderError(f"Provider type not supported: {provider_type}")

    def _create_aws_strategy(self) -> AWSProviderStrategy:
        """Create AWS provider strategy."""
        aws_config = self._config_manager.get_section("aws")
        return AWSProviderStrategy(
            config=AWSProviderConfig(**aws_config),
            logger=self._logger
        )

# Factory registration
def create_provider_strategy_factory(container: DIContainer) -> ProviderStrategyFactory:
    """Factory function for provider strategy factory."""
    return ProviderStrategyFactory(
        config_manager=container.get(ConfigurationPort),
        logger=container.get(LoggingPort)
    )

Lifecycle Management

The DI container manages different object lifecycles:

Singleton Lifecycle

# Registered once, same instance returned for all requests
container.register_singleton(ApplicationService)

# Usage - same instance every time
service1 = container.get(ApplicationService)
service2 = container.get(ApplicationService)
assert service1 is service2  # True

Transient Lifecycle

# New instance created for each request
container.register_transient(RequestHandler, RequestHandlerImpl)

# Usage - different instances
handler1 = container.get(RequestHandler)
handler2 = container.get(RequestHandler)
assert handler1 is not handler2  # True

Factory Lifecycle

# Factory function called for each request
container.register_factory(
    DatabaseConnection,
    lambda c: create_database_connection(c.get(ConfigurationPort))
)

Testing with Dependency Injection

The DI system enables easy testing through dependency substitution:

Unit Testing

def test_application_service():
    """Test application service with mocked dependencies."""
    # Create test container
    container = DIContainer()

    # Register mocks
    mock_command_bus = Mock(spec=CommandBus)
    mock_query_bus = Mock(spec=QueryBus)
    mock_logger = Mock(spec=LoggingPort)

    container.register_instance(CommandBus, mock_command_bus)
    container.register_instance(QueryBus, mock_query_bus)
    container.register_instance(LoggingPort, mock_logger)

    # Register service under test
    container.register_singleton(ApplicationService)

    # Get service with injected mocks
    service = container.get(ApplicationService)

    # Test service behavior
    # Mocks can be verified for interactions

Integration Testing

def test_with_real_dependencies():
    """Test with real dependencies but test configuration."""
    container = DIContainer()

    # Register real implementations with test configuration
    container.register_singleton(
        ConfigurationPort,
        lambda c: TestConfigurationAdapter()
    )

    # Register other services normally
    register_core_services(container)
    register_provider_services(container)

    # Test with real implementations
    service = container.get(ApplicationService)
    # Test actual behavior

Configuration-Driven Registration

Services can be registered based on configuration:

def register_storage_services(container: DIContainer) -> None:
    """Register storage services based on configuration."""
    config = container.get(ConfigurationPort)
    storage_type = config.get("storage.type", "memory")

    if storage_type == "dynamodb":
        container.register_singleton(
            TemplateRepository,
            DynamoDBTemplateRepository
        )
    elif storage_type == "memory":
        container.register_singleton(
            TemplateRepository,
            InMemoryTemplateRepository
        )
    else:
        raise ConfigurationError(f"Unsupported storage type: {storage_type}")

Registry Pattern Integration

The DI system integrates with the registry pattern for strategy-based component selection:

Scheduler Port Registration

The scheduler port uses the registry pattern for configuration-driven strategy selection:

# src/infrastructure/di/port_registrations.py
def register_port_adapters(container):
    """Register all port adapters in the DI container."""

    # Register scheduler port adapter using registry pattern
    def create_scheduler_port(c):
        from src.infrastructure.registry.scheduler_registry import get_scheduler_registry
        from src.config.manager import get_config_manager

        config_manager = get_config_manager()
        scheduler_config = config_manager.get_scheduler_config()
        scheduler_type = scheduler_config.get('strategy', 'hostfactory')

        registry = get_scheduler_registry()
        return registry.get_active_strategy(scheduler_type, scheduler_config)

    container.register_singleton(
        SchedulerPort,
        create_scheduler_port
    )

This pattern provides several benefits:

  • Configuration-Driven: Scheduler strategy selected based on configuration
  • Registry Integration: Leverages the scheduler registry for strategy management
  • Lazy Loading: Strategy created only when needed
  • Type Safety: Maintains appropriate port/adapter abstraction
  • Testability: Easy to mock or substitute strategies for testing

Registry Factory Pattern

The registry pattern enables dynamic strategy creation:

# Registry resolves strategy based on configuration
registry = get_scheduler_registry()
strategy = registry.get_active_strategy(scheduler_type, config)

# Strategy implements the port interface
assert isinstance(strategy, SchedulerPort)

This approach separates strategy selection logic from dependency injection, maintaining clean separation of concerns while enabling flexible configuration-driven behavior.

Error Handling

The DI system provides comprehensive error handling:

class DependencyInjectionError(Exception):
    """Base exception for DI errors."""
    pass

class DependencyNotFoundError(DependencyInjectionError):
    """Raised when a dependency cannot be resolved."""
    pass

class CircularDependencyError(DependencyInjectionError):
    """Raised when circular dependencies are detected."""
    pass

class RegistrationError(DependencyInjectionError):
    """Raised when service registration fails."""
    pass

Benefits of DI Implementation

Loose Coupling

  • Components depend on abstractions, not implementations
  • Easy to swap implementations
  • Reduced coupling between components

Testability

  • Easy mocking of dependencies
  • Isolated unit testing
  • Integration testing with different configurations

Maintainability

  • Clear dependency relationships
  • Single responsibility principle enforcement
  • Easy to understand and modify

Flexibility

  • Runtime behavior modification through configuration
  • Easy addition of new implementations
  • Support for different deployment scenarios

This comprehensive dependency injection system provides the foundation for clean, testable, and maintainable code throughout the Open Host Factory Plugin.