Status: Accepted
Date: 2026-01-06
Deciders: Kevin Chen
Tags: architecture, storage, helixdb, dependency-injection
helix-tools uses HelixDB for persistent graph and vector storage. The question is: how should tools integrate with HelixDB?
Option A (rejected): Every tool directly depends on HelixDB
- Tight coupling
- Tools can't be tested without HelixDB
- Shared crates become HelixDB-aware
Option B (accepted): Trait-based architecture with dependency injection
- Loose coupling
- Tools define their own storage interfaces
- HelixDB is an implementation detail, not a dependency
- Testability — Tools should be testable without external dependencies
- Loose coupling — Avoid forcing HelixDB into every crate
- Dependency inversion — High-level modules shouldn't depend on low-level modules
- Flexibility — Easy to add alternative implementations (memory, mock, etc.)
- Clarity — Each tool's storage needs should be explicitly defined
Each tool defines its own storage trait. HelixDB implementations are provided separately.
tool/
├── src/
│ ├── storage/
│ │ ├── mod.rs # trait DecisionStore { ... }
│ │ ├── helix.rs # impl DecisionStore for HelixBackend
│ │ └── memory.rs # impl DecisionStore for MemoryBackend (tests)
│ └── lib.rs
// storage/mod.rs — The trait (no HelixDB dependency)
pub trait DecisionStore: Send + Sync {
fn insert(&self, decision: &Decision) -> Result<()>;
fn get(&self, id: &str) -> Result<Option<Decision>>;
fn search(&self, query_embedding: &[f32], limit: usize) -> Result<Vec<SearchResult>>;
fn get_chain(&self, id: &str) -> Result<Vec<Decision>>;
fn get_related(&self, id: &str) -> Result<Vec<(Decision, RelationType)>>;
}
// storage/helix.rs — HelixDB implementation
pub struct HelixDecisionStore {
engine: HelixGraphStorage,
}
impl DecisionStore for HelixDecisionStore {
fn insert(&self, decision: &Decision) -> Result<()> {
// Uses HelixDB's native graph storage
// LMDB persistence is built-in
}
// ...
}
// storage/memory.rs — In-memory implementation for tests
pub struct MemoryDecisionStore {
decisions: HashMap<String, Decision>,
embeddings: HashMap<String, Vec<f32>>,
}
impl DecisionStore for MemoryDecisionStore { ... }| Crate | Purpose | HelixDB Dependency |
|---|---|---|
| helix-config | Configuration loading | NO — just documents paths |
| helix-id | ID generation | NO — pure utility |
| helix-embeddings | Text embeddings | NO — returns Vec<f32> |
| helix-discovery | Git root discovery | NO — pure utility |
Consumers decide what to do with embeddings. helix-embeddings doesn't know or care about storage.
- Testability — Tests use
MemoryDecisionStore, no HelixDB needed - Loose coupling — Tools work with trait interface, not HelixDB directly
- Clear contracts — Each tool's storage needs are explicitly defined
- Flexibility — Easy to add backends (SQLite, Postgres, etc.) later
- No shared HelixDB dependency — Shared crates stay pure
- More code — Each tool defines its own trait (but traits are small)
- No shared storage trait — Tools can't share a single
VectorStoretrait
- helix-storage removal — We don't need a shared storage abstraction; each tool defines its own
HelixDB already handles persistence via LMDB:
// From helix-db/src/helix_engine/storage_core/mod.rs
let graph_env = unsafe {
EnvOpenOptions::new()
.map_size(db_size * 1024 * 1024 * 1024)
.max_dbs(200)
.open(Path::new(path))? // Creates data.mdb + lock.mdb
};This means:
- Data persists automatically across runs
- No JSON serialization needed
- No "index rebuild" on startup
- Tools just open HelixDB at a path and data is there
Tools store HelixDB data in project-local directories:
{project}/.helix/data/{tool}/
├── data.mdb # LMDB data file
└── lock.mdb # LMDB lock file
helix-config documents HelixDB settings but doesn't depend on HelixDB:
# ~/.helix/config/config.toml
[helix_db]
map_size_mb = 1024
max_readers = 200Tools read this config and pass it to their HelixDB backend.
Rejected because:
- Different tools have different storage needs (graph traversal vs simple CRUD)
- Forces a lowest-common-denominator interface
- helix-storage was scaffolding, not a long-term solution
Rejected because:
- Tight coupling
- Can't test without HelixDB
- Violates dependency inversion
- ADR-003: Binary Installation Strategy (distribution)
- Future: ADR for embedding model selection