This ADR was written when the system used Neo4j for entity storage. As of March 2026, Neo4j has been fully removed and replaced by PostgreSQL taxonomy tables (taxonomy_entities, taxonomy_relationships). The decision rationale documented here remains valid; the storage layer has changed.
ADR-0011: Module Inlining
Status: Accepted
Context
The ZOL Intelligent Search system was initially developed as a multi-package architecture, with five separate Python packages each published and versioned independently:
| Package | Responsibility |
|---|---|
| s4u-llm-client | LLM provider abstraction and API client |
| s4u-llm-eval | Response evaluation framework |
| s4u-rag | RAG pipeline orchestration |
| s4u-knowledge-graph | Neo4j integration and entity management |
| s4u-document-processor | Document extraction and chunking |
This modular design was intended to promote reusability across projects and enforce clean separation of concerns. In practice, it created significant friction.
The Problem
Pain Points
-
Version coordination: A change in s4u-llm-client required updating its version, publishing it, updating the dependency in s4u-rag, publishing that, and updating the dependency in the application. A single logical change required 3+ repository updates.
-
Debugging difficulty: Stack traces spanning multiple packages were hard to follow. Setting breakpoints across package boundaries required editable installs of all packages.
-
CI/CD complexity: Each package needed its own CI pipeline, PyPI publishing step, and version management. The release process for a cross-cutting change involved coordinating 5+ pipelines.
-
No external consumers: The packages were designed for cross-project reuse, but no other project consumed any of these packages. The reusability benefit never materialized.
-
Dependency conflicts: Different packages occasionally pinned conflicting versions of shared dependencies (pydantic, httpx), requiring careful coordination.
Decision
Inline all five packages into a monorepo under the application's module structure:
Module Mapping
| Original Package | New Location | Notes |
|---|---|---|
| s4u-llm-client | app/llm/ | LLM provider abstraction |
| s4u-llm-eval | app/evaluation/ | Evaluation metrics |
| s4u-rag | app/services/rag/ | RAG pipeline |
| s4u-knowledge-graph | app/services/graph/ | Neo4j integration |
| s4u-document-processor | app/services/ingestion/ | Document processing |
Consequences
Positive
- Single install:
pip install -e .installs everything. No coordination needed. - Unified CI/CD: One pipeline builds, tests, and deploys the entire system
- Simplified debugging: Standard IDE debugging with breakpoints anywhere
- Atomic changes: A cross-cutting refactor is a single commit, not 5 coordinated releases
- No dependency conflicts: A single requirements.txt governs all dependencies
- Faster development: No publish-update-publish cycle for internal changes
Negative
- No cross-project reuse: If another project needs the LLM client, it cannot pip-install it independently. However, since no such consumer exists, this is a theoretical rather than practical loss.
- Larger repository: All code in one repo increases clone size and cognitive load
- Less enforced boundaries: Package boundaries enforced module separation; within a monorepo, discipline must be maintained through convention and code review
Before and After
The premature extraction of internal modules into separate packages is a common architectural pitfall. The inlining decision reflects the principle: do not optimize for reuse until reuse is demonstrated. Internal module boundaries (Python packages within a monorepo) provide sufficient separation of concerns without the operational overhead of independent packaging.