Skip to content

Dependency Inversion Pattern

Overview

The Dependency Inversion pattern enables flexible component composition and lifecycle management by defining abstractions that high-level components depend on, while low-level components implement these abstractions. In NERV, this pattern drives the component wiring system with explicit dependency declarations and resolution.

Key Concepts

  • Abstractions: Interfaces or protocols that components depend on
  • Implementations: Concrete classes that fulfill abstractions
  • Containers: Registries that manage component lifecycles and dependencies
  • Providers: Factories that create component instances on demand
  • Injectors: Mechanisms that supply dependencies to components

Benefits

  • Decoupling: Components depend on abstractions, not implementations
  • Testability: Dependencies can be easily substituted with test doubles
  • Configurability: System composition can be modified without code changes
  • Lifecycle Management: Component creation and disposal is centralized
  • Explicit Dependencies: Dependencies are clearly defined and discoverable

Implementation Considerations

  • Performance Overhead: Resolution of dependencies adds some runtime cost
  • Complexity: Additional infrastructure for component management
  • Type Safety: Ensuring correct dependencies at compile/runtime
  • Circular Dependencies: Managing or preventing circular reference issues
  • Scoping Rules: Determining component lifecycle and sharing policies

Implementation with Dependency Injector

The Dependency Inversion pattern in NERV is implemented using the Dependency Injector library, which provides a declarative and flexible dependency injection system for Python. It was chosen for the following reasons:

  1. Declarative API: Clean, declarative syntax for defining containers and providers
  2. Type Hinting Integration: Works well with Python type hints
  3. Performance: Efficient dependency resolution and caching
  4. Scoping Options: Support for different component lifecycles
  5. Runtime Configuration: Dynamic configuration capabilities

Core Dependency Injector Components

ComponentPurpose
ContainerRegistry for component providers and configuration
ProviderFactory for component creation with dependency injection
SingletonProvider that creates a single instance for the entire container
FactoryProvider that creates a new instance each time
ResourceProvider for resources requiring setup and cleanup

Wiring Mechanism

Dependency Injector automatically wires dependencies using type annotations:

python
# Type definitions only - not full implementation
from dependency_injector.wiring import inject, Provide

@inject
def process_request(
    provider_registry: ProviderRegistry = Provide[Container.provider_registry],
    event_bus: EventBus = Provide[Container.event_bus]
):
    """Function with automatically injected dependencies."""
    # Implementation using injected components

Pattern Variations

Hierarchical Container

Organizes dependencies in parent-child relationships for modular configuration.

python
# Type definitions only - not full implementation
from dependency_injector import containers, providers

class CoreContainer(containers.DeclarativeContainer):
    """Core service container."""
    config = providers.Configuration()
    event_bus = providers.Singleton(EventBus)

class AgentContainer(containers.DeclarativeContainer):
    """Agent component container with core dependencies."""
    core = providers.DependenciesContainer()
    agent_controller = providers.Singleton(
        AgentController,
        event_bus=core.event_bus
    )

Factory Provider System

Creates component instances with contextual configuration.

python
# Type definitions only - not full implementation
class ConfigurableProviderFactory:
    """Factory for creating configured provider instances."""

    @inject
    def __init__(self, config: Configuration = Provide[Container.config]):
        self.config = config

    def create_provider(self, provider_type, model_name):
        """Create provider instance with specific configuration."""
        provider_config = self.config.providers[provider_type]
        return providers.Factory(
            get_provider_class(provider_type),
            model_name=model_name,
            api_key=provider_config.api_key
        )

Dynamic Registration

Allows runtime component registration for extensibility.

python
# Type definitions only - not full implementation
class DynamicContainer(containers.DeclarativeContainer):
    """Container supporting runtime registration."""

    def register_component(self, name, component_class, dependencies=None):
        """Register a component at runtime."""
        dependencies = dependencies or {}
        # Create provider for component with dependencies
        provider = providers.Singleton(component_class, **dependencies)
        # Register provider in container
        setattr(self, name, provider)
        return provider

Component Lifecycle Management

The Dependency Inversion pattern carefully manages component lifecycles:

Singleton Components

Components with application-wide lifecycle, created only once.

python
# Type definitions only - not full implementation
class NERVContainer(containers.DeclarativeContainer):
    """Main NERV container."""

    # Singleton components with application lifecycle
    event_bus = providers.Singleton(EventBus)
    temporal_store = providers.Singleton(TemporalStore)
    state_projector = providers.Singleton(
        StateProjector,
        initial_state=providers.Dict(documents=providers.List())
    )

Factory Components

Components created on demand for specific operations.

python
# Type definitions only - not full implementation
class ProviderContainer(containers.DeclarativeContainer):
    """Provider component container."""

    # Factory components created per request
    anthropic_provider = providers.Factory(
        AnthropicProvider,
        api_key=providers.Callable(get_api_key, "ANTHROPIC_API_KEY")
    )

    openai_provider = providers.Factory(
        OpenAIProvider,
        api_key=providers.Callable(get_api_key, "OPENAI_API_KEY")
    )

Resource Components

Components requiring explicit resource management.

python
# Type definitions only - not full implementation
class ResourceContainer(containers.DeclarativeContainer):
    """Resource management container."""

    # Database connection with proper lifecycle
    database = providers.Resource(
        Database,
        connection_string=providers.Callable(get_connection_string),
        # Resource cleanup after use
        cleanup=lambda db: db.close()
    )

Integration with Other NERV Patterns

Integration with Reactive Event Mesh

python
# Type definitions only - not full implementation
class EventDrivenContainer(containers.DeclarativeContainer):
    """Container for event-driven architecture."""

    # Event bus as central component
    event_bus = providers.Singleton(EventBus)

    # Components depending on event bus
    provider_manager = providers.Singleton(
        ProviderManager,
        event_bus=event_bus
    )

    agent_controller = providers.Singleton(
        AgentController,
        event_bus=event_bus
    )

    # Event listeners automatically wired
    @containers.wire(modules=[".listeners"])
    def wire_listeners(self):
        """Wire event listener functions."""
        pass

Integration with Temporal Versioning

python
# Type definitions only - not full implementation
class VersionedContainer(containers.DeclarativeContainer):
    """Container with versioned components."""

    # Temporal store for versioning
    temporal_store = providers.Singleton(TemporalStore)

    # Factory for versioned repositories
    versioned_repository = providers.Factory(
        VersionedRepository,
        temporal_store=temporal_store
    )

    # Document repository with versioning
    document_repository = providers.Singleton(
        DocumentRepository,
        versioned_repository=providers.Factory(
            versioned_repository,
            entity_type="document"
        )
    )

Integration with Effect System

python
# Type definitions only - not full implementation
class EffectDrivenContainer(containers.DeclarativeContainer):
    """Container for effect-driven components."""

    # Effect handler registry
    effect_handler = providers.Singleton(EffectHandler)

    # Register effect handlers
    @providers.init
    def init_handlers(self):
        """Initialize and register effect handlers."""
        self.effect_handler().register_handler(
            EffectType.NETWORK,
            providers.Factory(NetworkEffectHandler)
        )
        self.effect_handler().register_handler(
            EffectType.DATABASE,
            providers.Factory(DatabaseEffectHandler)
        )

Performance Considerations

When implementing the Dependency Inversion pattern, consider these performance factors:

  1. Resolution Caching: Cache resolved dependencies for better performance
  2. Lazy Initialization: Create components only when needed
  3. Scoping Strategy: Choose appropriate lifecycle scopes for components
  4. Compile-time Wiring: Generate wiring code at build time when possible
  5. Injection Points: Minimize the number of injection points for critical paths
  • Inversion of Control
  • Factory Method
  • Abstract Factory
  • Service Locator
  • Strategy Pattern

Implementation Reference

See the implementation component: Container

Released under the MIT License.