Skip to content

Dependency Injection

Dependency injection is the first principle in Memosa’s foundation rules, and it is not optional. Every component declares what it needs in its constructor, and something external — a factory function for production, a test for fakes — supplies those dependencies. No class reaches out and builds its own collaborators.

This page states the rules, explains why they exist, and walks the three production wiring layers in src/di/.

From .claude/rules/00-foundation.md, the DI contract every component obeys:

  • Inject dependencies via constructors. A component receives its collaborators as constructor arguments.
  • Never create dependencies internally. A class must not instantiate a vector store, an LLM client, a Redis connection, or any other service inside itself.
  • No default values for dependencies. A dependency is required and explicit; it does not silently fall back to a globally constructed instance.
  • Factory functions for production wiring. The concrete, production-configured object graph is assembled in factory functions under src/di/, not scattered through the codebase.
  • Fakes in tests. Tests inject fakes (or doubles) for external dependencies; tests must not depend on external services like Pinecone, Redis, or a live LLM.

DI is enforced strictly because of what Memosa is: a production system with external dependencies that are slow, expensive, stateful, or all three.

  • Testability without external services. The system talks to Pinecone, Redis, PostgreSQL, Voyage, and Anthropic. Tests must run deterministically and offline, which is only possible if every one of those can be replaced by a fake at the constructor boundary. Coverage targets (>85% overall, 100% on critical paths) are unreachable otherwise.
  • Namespace and tenant isolation. Isolation is a correctness boundary. Injecting the namespace-aware collaborators (rather than letting a component reach for a global) keeps the explicit-namespace discipline intact through the whole call chain.
  • Single LLM provisioning point. Direct ChatOpenAI / model instantiation is banned project-wide. All LLM clients come from ProductionLLMFactory, so model choice is tied to the task class (Sonnet for domain work, Opus for quality-critical paths) rather than to whoever wrote a service most recently. DI is how that single point of provisioning actually reaches every consumer.
  • Lifecycle and connection management. Pooled, singleton, and per-loop resources (the PostgreSQL pool, the deal store, Redis clients) are constructed once in the wiring layer and shared correctly. Letting components build their own would leak connections and break the pool budgets each Railway service operates under.

In short: DI is what makes the rest of the architecture — isolation, graceful degradation, deterministic tests — achievable rather than aspirational.

Production wiring lives in src/di/, organized into three layers that build on one another. Each layer’s factories depend only on layers beneath it.

1. Infrastructure (infrastructure.py + core_services.py)
│ base services: file system, OCR, LLM clients,
│ vector store, deal store, namespace manager
2. Document (document_processors.py)
Processing PDF / Excel / CoStar processors + DocumentOrchestrator,
│ each built on infrastructure services
3. Agentic (langgraph_agents.py)
(LangGraph) deep agents + DealAnalysisOrchestrator, built on
processors, the vector store, and the LLM factory

src/di/main.py sits on top and composes all three into the full production object graph that the worker and Canvas consume.

Layer 1 — Infrastructure (infrastructure.py)

Section titled “Layer 1 — Infrastructure (infrastructure.py)”

Base services with few or no dependencies of their own: the file system, PDF utilities, the image processor, the OCR client, the chat-completion clients, the structured-data extractor, the namespace manager, the vector store, and the asynchronous DealStoreService (with its connection-pool stabilization and singleton caching).

This layer also defines the three chat-completion client factories, each tied to a task class so model choice is intentional:

  • get_production_vision_chat_completion_client — Opus, for multimodal image analysis; deliberately opted out of the shared LLM circuit breaker so vision errors do not cascade into domain subgraphs.
  • get_production_domain_chat_completion_client — Sonnet, for domain and analytical reasoning (subgraph content, tool calls, structured-output JSON); shared circuit breaker attached.
  • get_production_anthropic_chat_completion_client — Opus, for quality-critical paths (synthesis, the TIER 3 critique) where reasoning accuracy outweighs speed.

A representative infrastructure factory shows the pattern — it builds a dependency and injects it, rather than letting the consumer construct its own OCR client:

def get_production_image_processor_service() -> IImageProcessor:
"""Returns an ImageProcessorService instance with injected OCR client."""
ocr_client = get_production_ocr_client_service()
return ImageProcessorService(mistral_ocr_client=ocr_client)

ImageProcessorService never creates an OCR client itself; it accepts one. In a test, a fake OCR client is passed to the same constructor.

Layer 2 — Document Processing (document_processors.py)

Section titled “Layer 2 — Document Processing (document_processors.py)”

Factories for the three processors and the orchestrator that coordinates them, each wired from the infrastructure layer. For example, get_production_pdf_orchestrator assembles a PDFOrchestrator from the file-system service, PDF utilities, the image processor, the OCR client, the structured-data extractor, a domain chat-completion client, and (when a vector store is provided) the image-intelligence subgraph and the deal store:

return PDFOrchestrator(
file_system=file_system_service,
pdf_utils_service=pdf_utils_service,
image_processor=image_processor_service,
ocr_client=ocr_client_service,
structured_data_extractor=structured_data_extractor,
chat_completion_client=chat_completion_client,
pdf_processor=pdf_processor,
image_intelligence_subgraph=image_intelligence_agent,
deal_store=deal_store_service,
)

get_production_document_orchestrator then takes the three constructed processors as arguments and returns a DocumentOrchestrator — pure injection, no internal construction of the processors it coordinates.

Layer 3 — Agentic / LangGraph (langgraph_agents.py)

Section titled “Layer 3 — Agentic / LangGraph (langgraph_agents.py)”

The deep research agents and the DealAnalysisOrchestrator. The central factory, get_production_langgraph_deal_analysis_orchestrator, constructs every subgraph internally and injects the shared infrastructure: the vector store, the PostgreSQL checkpointer pool, the ProductionLLMFactory, the discourse logger, the feedback memory, the Redis client, and the learning services (signals, trend, style, expertise, patterns).

Each domain agent has its own production factory (for example get_production_risk_deep_agent, get_production_property_deep_agent) that injects the retrieval engine, the budget manager, the LLM factory, the deal store, and the deterministic default namespace "__unset__" — the sentinel that forces callers to propagate a real namespace at call time rather than defaulting to the wrong deal. Agents all inherit BaseDeepAgent, so circuit-breaking, budget handling, tool-context lifecycle, and memory wiring are shared rather than duplicated per agent.

src/di/main.py is the composition root. get_production_services() walks the layers in order — Redis client, vector store, processors, document orchestrator, the intel/learning services, then the deal-analysis orchestrator and the notification components — and returns a single services dictionary that the worker and Canvas consume. The result is cached per process ID so each process builds the object graph exactly once.

services = {
"redis_client": redis_client,
"vector_store": vector_store,
"document_orchestrator": document_orchestrator,
"deal_analysis_orchestrator": orchestrator,
# ... learning services, dispatchers, notifiers
}

A separate get_worker_services() provides a deliberately minimal set for lightweight worker processes that do not need the full analysis graph.

Because every dependency is a constructor argument, a test constructs the component under test directly and passes fakes. There is no global to monkey-patch and no real service to stand up:

# Production wiring uses the factory:
processor = get_production_image_processor_service() # injects the real OCR client
# A test bypasses the factory and injects a fake:
processor = ImageProcessorService(mistral_ocr_client=FakeOCRClient())

The same constructor serves both paths. That symmetry — identical construction surface for production and test — is the whole point of the rules at the top of this page.

DI shows up in the project’s banned-pattern list in several forms. The ones to internalize:

  • Direct ChatOpenAI / model instantiation — always go through ProductionLLMFactory.
  • Inline construction of learning services (SignalAggregator, ExpertiseService, PatternsService) per request — inject them at DI time instead; inline construction leaks Redis connections and bypasses tenancy guards.
  • No default values for dependencies — a missing dependency should be an explicit wiring decision, surfaced at construction, not silently filled by a global.
  • .claude/rules/00-foundation.md — Core Principle 1 (Dependency Injection, non-negotiable) and the testing/error-handling principles it supports.
  • .claude/rules/10-domains.md — the three DI layers (Infrastructure → Processing → Agentic) and the src/di/ directory listing.
  • .claude/rules/20-patterns.md — DI layering and the anti-patterns (direct ChatOpenAI, inline learning-service construction).
  • src/di/infrastructure.py — Layer 1 factories: get_production_image_processor_service (the example), the three chat-completion client factories, and get_production_deal_store_service.
  • src/di/document_processors.py — Layer 2 factories: get_production_pdf_orchestrator, get_production_document_orchestrator, and the processor factories.
  • src/di/langgraph_agents.py — Layer 3 factories: get_production_langgraph_deal_analysis_orchestrator and the per-domain agent factories (get_production_risk_deep_agent, etc.).
  • src/di/main.py — the composition root: get_production_services (full graph, per-PID cache) and get_worker_services (minimal set).
  • src/di/__init__.py — confirms there is no re-export shim; consumers import directly from the sub-modules.