7.4 KiB
adr_number, title, status_history, tier, authors, superseded_by, related_adrs, acceptance
| adr_number | title | status_history | tier | authors | superseded_by | related_adrs | acceptance | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 3 | Dependency Injection |
|
1 |
|
null |
|
|
Context
The layered architecture (ADR-001) requires that outer layers depend on inner layers through abstract interfaces, never through concrete implementations. Services in the Application Layer need repositories, LLM providers, indexing backends, and sandbox managers — but must not instantiate them directly. Testing requires replacing real infrastructure with mocks. Runtime reconfiguration (e.g., switching LLM providers or swapping database backends between local and server modes) must be possible without modifying application or domain code.
A dependency injection mechanism is needed to wire all components together, enforce the layer boundary rules, and support both production and test configurations.
Decision Drivers
- Layered architecture requires outer layers to depend on inner layers through abstract interfaces, never concrete implementations
- Application Layer services need repositories, LLM providers, and sandbox managers without instantiating them directly
- Testing requires replacing real infrastructure with mocks without modifying application or domain code
- Runtime reconfiguration (switching LLM providers or database backends between deployment modes) must not require code changes
- All cross-layer wiring must be explicit and auditable from a single location
Decision
All service dependencies are wired through a DeclarativeContainer from the dependency-injector library (>= 4.41.0). This container is the single composition root for the entire application. No service instantiates its own dependencies — all are received through constructor injection managed by the container.
Design
Container Structure
The DI container is a DeclarativeContainer subclass that declares all providers:
Singletonproviders for long-lived services: database engine, repository implementations, event bus, index backends, provider registry.Factoryproviders for per-request or per-operation objects: sandbox instances, plan execution contexts, LangGraph state graphs.Configurationprovider for loading and exposing the global TOML configuration and environment variables.
Wiring Pattern
- The Domain Layer defines repository interfaces as
Protocolclasses (e.g.,PlanRepository,DecisionRepository,ResourceRepository). - The Infrastructure Layer provides concrete implementations (e.g.,
SQLAlchemyPlanRepository,SQLAlchemyDecisionRepository). - The DI container maps protocols to implementations using
SingletonorFactoryproviders. - Application Layer services declare their dependencies as constructor parameters typed to the domain protocols.
- The container auto-wires these dependencies at application startup.
Provider Types Used
| Provider Type | Use Case | Example |
|---|---|---|
Singleton |
Shared stateful services | Database session factory, event bus, index backends |
Factory |
Per-use transient objects | Sandbox instances, execution contexts |
Configuration |
External configuration values | TOML config keys, environment variables |
Testing Support
In test configurations, the container is reconfigured to supply mock or fake implementations:
FakeListLLM/FakeEmbeddingsfrom LangChain Community replace real LLM providers.- In-memory repositories replace SQLAlchemy-backed repositories.
- No-op sandbox strategies replace git worktree or filesystem copy strategies.
The container's override() context manager allows swapping providers for individual test cases without affecting the global configuration.
Constraints
- All cross-layer dependencies must flow through the DI container. Direct instantiation of infrastructure components in application or domain code is prohibited.
- The container is initialized exactly once at application startup (in the CLI entry point or server bootstrap). It must not be re-initialized during request handling.
- Domain Layer code must never import or reference the
dependency-injectorlibrary. Domain protocols are plain PythonProtocolclasses with no DI framework annotations. - Every new service or repository must be registered in the container before use. Unregistered dependencies cause explicit startup failures, not silent runtime errors.
Consequences
Positive
- Hidden coupling between layers is eliminated — all dependencies are explicitly declared and visible in the container definition.
- Testing with mock implementations is straightforward via container overrides.
- Runtime reconfiguration (e.g., switching from SQLite to PostgreSQL, or from FAISS to Qdrant) requires changing only the container provider mapping.
- The container serves as living documentation of all system components and their relationships.
Negative
- Adds a third-party library dependency (
dependency-injector) to the project. - Container configuration can become large and complex as the system grows.
- Developers unfamiliar with DI patterns may find the indirection confusing initially.
Risks
- Over-reliance on the container for fine-grained objects (e.g., value objects, data transfer objects) would add unnecessary complexity. The container should be reserved for service-level dependencies.
- If the container definition drifts out of sync with actual usage, runtime wiring errors may only surface at startup.
Alternatives Considered
Manual constructor injection without a framework — Simpler but does not scale. Without a framework, the composition root becomes a large imperative block of service = Service(dep1=Dep1(...), dep2=Dep2(...)) calls that is difficult to maintain, test-override, and reconfigure at runtime.
Python-native approaches (importlib, module-level singletons) — Insufficient for the requirement of swappable implementations across deployment modes. Module-level singletons create hard-wired dependencies that cannot be overridden for testing or reconfiguration.
Compliance
- Container completeness check: A startup self-test verifies that all declared providers can be resolved without errors. This runs as part of the application bootstrap and in CI.
- Import linting: CI rules verify that
domainmodules do not import fromdependency_injector. - Code review: New services must include their container registration in the same pull request that introduces them.
- Test coverage: Integration tests verify that the production container configuration resolves all dependencies correctly. Test configurations verify that override mechanisms work as expected.