ADR-002: Dependency Injection Framework
Context
The Tasteful framework needed a robust dependency injection system to:- Manage service lifecycles across flavors
- Enable testability through easy mocking and overrides
- Provide configuration management for different environments
- Support complex dependency graphs between services
- Maintain type safety where possible
Decision
We chose thedependency-injector library as our IoC (Inversion of Control) container framework.
Key Features Utilized
- Declarative Containers: Clean definition of dependencies
- Provider Types: Factory, Singleton, Configuration providers
- Container Overriding: Easy testing and environment-specific configurations
- Provider Traversal: Ability to inspect and manage the dependency graph
- Wiring: Automatic injection into functions and methods
Implementation Pattern
Tasteful implements a sophisticated dependency injection system that combines the power of thedependency-injector library with graph-based dependency resolution:
Container Architecture: Each flavor maintains its own declarative container that automatically registers services and repositories based on constructor dependencies. This eliminates the need for manual provider configuration while maintaining full control over the dependency graph.
Graph-Based Resolution: The system uses a dependency graph to analyze constructor signatures and determine the correct order for dependency registration. This ensures that dependencies are available when needed and prevents circular dependency issues.
Automatic Registration: Services and repositories are automatically registered as singleton providers in the container, with their constructor dependencies resolved and injected automatically.
Service Registration Pattern
The framework employs an intelligent service registration mechanism that: Analyzes Constructor Signatures: Uses Python’s inspection capabilities to examine constructor parameters and their type annotations to understand dependency requirements. Resolves Dependencies: Automatically identifies which dependencies are available in the injectable classes list and creates the appropriate provider relationships. Creates Providers: Dynamically creates singleton providers for each service, ensuring proper lifecycle management and dependency injection. Maintains Type Safety: Preserves type annotations and IDE support while providing runtime dependency resolution.Consequences
Positive
- Testability: Easy to mock dependencies and override containers for testing
- Flexibility: Can swap implementations based on environment or configuration
- Type Safety: Maintains Python type hints and IDE support
- Performance: Efficient lazy loading and singleton management
- Documentation: Self-documenting dependency graphs
- Debugging: Clear dependency resolution path
Negative
- Learning Curve: Team needs to understand DI concepts and this specific library
- Boilerplate: Requires container definitions for each module
- Runtime Complexity: Additional abstraction layer
- Debugging Complexity: Dependency resolution errors can be harder to trace
Trade-offs
- Flexibility vs. Simplicity: More powerful but requires more setup
- Runtime vs. Compile-time: Some dependency issues only surface at runtime
- Explicit vs. Implicit: Clear dependency declarations but more verbose
Alternatives Considered
-
Manual Dependency Injection: Constructor injection without framework
- Rejected: Too much boilerplate and error-prone for complex graphs
-
injectorlibrary: Simpler DI framework- Rejected: Less mature, fewer features for configuration management
-
FastAPI’s built-in dependency injection: Using Depends()
- Considered: Good for HTTP layer but not suitable for service layer architecture
-
Service Locator Pattern: Global registry of services
- Rejected: Anti-pattern that hides dependencies