---
adr_number: 3
title: Dependency Injection
status_history:
- - '2026-02-16'
  - Proposed
  - Jeffrey Phillips Freeman
- - '2026-02-16'
  - Accepted
  - Jeffrey Phillips Freeman
tier: 1
authors:
- Jeffrey Phillips Freeman
superseded_by: null
related_adrs:
- number: 1
  title: Layered Architecture
  relationship: DI enforces the layered dependency rule by wiring outer-to-inner dependencies
- number: 5
  title: Technical Stack
  relationship: Specifies `dependency-injector` as the chosen DI framework
- number: 19
  title: Storage and Persistence
  relationship: Repository implementations are injected via the DI container
- number: 22
  title: LangChain/LangGraph Integration
  relationship: LLM providers and LangGraph components are registered through the DI container
acceptance:
  votes_for:
  - voter: Jeffrey Phillips Freeman <Jeffrey.Freeman@CleverThis.com>
    comment: A single composition root with declarative containers keeps wiring explicit and testable
  votes_against: []
  abstentions: []
---
## 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:

- **`Singleton` providers** for long-lived services: database engine, repository implementations, event bus, index backends, provider registry.
- **`Factory` providers** for per-request or per-operation objects: sandbox instances, plan execution contexts, LangGraph state graphs.
- **`Configuration` provider** for loading and exposing the global TOML configuration and environment variables.

### Wiring Pattern

1. The Domain Layer defines repository interfaces as `Protocol` classes (e.g., `PlanRepository`, `DecisionRepository`, `ResourceRepository`).
2. The Infrastructure Layer provides concrete implementations (e.g., `SQLAlchemyPlanRepository`, `SQLAlchemyDecisionRepository`).
3. The DI container maps protocols to implementations using `Singleton` or `Factory` providers.
4. Application Layer services declare their dependencies as constructor parameters typed to the domain protocols.
5. 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` / `FakeEmbeddings` from 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-injector` library. Domain protocols are plain Python `Protocol` classes 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 `domain` modules do not import from `dependency_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.
