Skip to main content

ADR-006: Graph-Based Dependency Injection

Context

The framework’s dependency injection required manual ordering of dependencies, making it error-prone and difficult to debug. Complex applications needed automatic dependency resolution with circular dependency detection.

Decision

Implemented a graph-based dependency resolution system that automatically analyzes constructor dependencies and resolves them in correct order with built-in circular dependency detection.

Key Components

  • Dependency Graph Construction: Automatic analysis of constructor signatures
  • Topological Sorting: Correct dependency resolution order using graph traversal
  • Circular Dependency Detection: Early detection with clear error messages
  • Automatic Registration: Seamless integration with existing DI container

Core Algorithm

def _resolve_dependencies(self, node: Node) -> None:
    """Resolve dependencies using DFS with cycle detection."""
    self.unresolved.append(node.target_class)
    
    for dependency_node in node.dependencies:
        if dependency_node.target_class not in self.resolved:
            if dependency_node.target_class in self.unresolved:
                raise CircularDependencyError(
                    f"Circular dependency: {node.target_class.__name__} → "
                    f"{dependency_node.target_class.__name__}"
                )
            self._resolve_dependencies(node=dependency_node)
    
    self.resolved.append(node.target_class)
    self.unresolved.remove(node.target_class)

Consequences

Positive

  • Eliminates manual dependency ordering
  • Prevents circular dependencies with clear error messages
  • Improves debugging with clear dependency chains
  • Maintains type safety through annotations
  • Scales efficiently with complex applications

Negative

  • Slight startup overhead for graph construction
  • Additional memory for graph structure
  • Requires proper type annotations
  • Less explicit control over resolution order

Usage Example

class TotoService(BaseService):
    def __init__(self, tata_service: TataService, toto_repository: TotoRepository):
        self.tata_service = tata_service
        self.toto_repository = toto_repository

class TotoFlavor(BaseFlavor):
    def __init__(self):
        super().__init__(
            services=[TotoService, TataService, TitiService],  # Order doesn't matter
            repositories=[TotoRepository],
        )
The system automatically resolves dependencies in correct order regardless of registration sequence.
I