ADR-003: DataRepository Facade Pattern (Deprecated)
Status: Deprecated (maintained for backward compatibility) Date: 2024 (initial), Deprecated during Phase 1 refactoring Deciders: BrainDrive Team Tags: architecture, refactoring, anti-corruption-layer, technical-debt
Context
Original architecture had single DataRepository class handling all API calls:
- Collections CRUD
- Documents CRUD
- Chat sessions CRUD
- Chat messages CRUD
- RAG search queries
- Health checks
Problem discovered:
- Single class grew to 800+ lines
- Violated Single Responsibility Principle
- Hard to test (too many dependencies)
- Hard to understand (mixed concerns)
- Difficult to mock for component tests
Refactoring goal (Phase 1):
- Extract specialized repositories (CollectionRepository, DocumentRepository, etc.)
- Introduce HttpClient abstraction (anti-corruption layer)
- Improve testability
- Maintain backward compatibility (non-breaking migration)
Problem Statement
How do we refactor DataRepository without breaking existing code?
Options:
- Big bang: Replace all DataRepository usage at once
- Facade pattern: Keep DataRepository as facade, delegate to specialized repos
- Deprecation warnings: Mark old methods deprecated, force migration
- Parallel implementation: New code uses repos, old code uses DataRepository
Requirements:
- Non-breaking migration path
- Gradual refactoring (phase-by-phase)
- No downtime
- Components can migrate at their own pace
Decision
Chosen approach: Facade pattern with deprecation
Architecture:
Components
│
├───> DataRepository (facade) ──> Specialized Repositories
│ ├── CollectionRepository
│ ├── DocumentRepository
│ ├── ChatSessionRepository
│ ├── ChatMessageRepository
│ └── RAGRepository
│
└───> HttpClient ─────────────────> BrainDrive API / fetch
Implementation pattern:
// DataRepository.ts (facade)
export class DataRepository {
private collectionRepo: CollectionRepository;
private documentRepo: DocumentRepository;
// ... other repos
constructor(apiService?: APIService) {
const httpClient = new HttpClient(apiService);
this.collectionRepo = new CollectionRepository(httpClient);
this.documentRepo = new DocumentRepository(httpClient);
// ...
}
// Facade methods delegate to specialized repos
async getCollections(): Promise<Collection[]> {
return this.collectionRepo.getCollections();
}
async getDocuments(collectionId: string): Promise<Document[]> {
return this.documentRepo.getDocuments(collectionId);
}
// ... more delegations
}
Rationale:
- Non-breaking: All existing calls still work
- Gradual migration: New code can use specialized repos directly
- Clear deprecation path: Facade marked as legacy, will be removed later
- Testability: Specialized repos are small, focused, easy to test
- Single source: Logic in repos, facade just delegates (no duplication)
Consequences
Positive
- ✅ Non-breaking migration (zero downtime)
- ✅ Specialized repos are testable (16 tests added in Phase 1)
- ✅ Clear separation of concerns (SRP)
- ✅ HttpClient provides anti-corruption layer (BrainDrive API vs fetch)
- ✅ New code can skip facade, use repos directly
- ✅ Gradual migration (components update at their own pace)
Negative
- ❌ Extra indirection (facade → repo → HttpClient → API)
- ❌ Temporary code duplication (facade + repos)
- ❌ "Deprecated but still used" feels awkward
- ❌ Developers might be confused which to use
- ❌ Will need removal later (tech debt)
Risks
- Facade never removed: Becomes permanent tech debt
- Mitigation: Track usage, remove when all components migrated
- Confusion about which to use: New devs use facade instead of repos
- Mitigation: Documentation, code reviews, deprecation warnings
- Testing both paths: Must test facade AND repos
- Mitigation: Facade tests minimal, repo tests comprehensive
Neutral
- Two ways to do same thing (temporary state during refactoring)
- Repo pattern well-established (similar to Martin Fowler's patterns)
Alternatives Considered
Alternative 1: Big Bang Replacement
Description: Update all components to use specialized repos in one PR
Pros:
- Clean architecture immediately
- No temporary facade
- Clear migration (done in one shot)
Cons:
- High risk (1000+ line PR)
- Hard to review
- Breaks if anything missed
- Blocks other work during migration
Why rejected: Too risky, blocks parallel development
Alternative 2: Deprecation Warnings Only
Description: Mark methods deprecated, let TypeScript warn, force migration
Pros:
- Forces cleanup
- No double implementation
- Clear pressure to migrate
Cons:
- Breaking change (immediate failures in components)
- Requires all components update at once
- Blocks feature development during migration
Why rejected: Too disruptive, not gradual enough
Alternative 3: Parallel Namespaces
Description: Keep DataRepository unchanged, add new RepositoryFactory for repos
Pros:
- Completely non-breaking
- Clear separation (old vs new)
- Can coexist indefinitely
Cons:
- Duplicates logic (old methods still have implementation)
- Higher maintenance burden
- No pressure to migrate (old code never cleaned)
Why rejected: Permanent duplication, no cleanup path
References
- Phase 1 refactoring: Extract specialized repositories from monolithic DataRepository
- src/braindrive-plugin/DataRepository.ts (facade implementation)
- src/infrastructure/repositories/ (specialized repos)
- src/infrastructure/http/HttpClient.ts (anti-corruption layer)
- Related: Phase 1 added 16 repository tests
Implementation Notes
File paths affected:
src/braindrive-plugin/DataRepository.ts- Converted to facadesrc/infrastructure/http/HttpClient.ts- New abstractionsrc/infrastructure/repositories/CollectionRepository.ts- Extractedsrc/infrastructure/repositories/DocumentRepository.ts- Extractedsrc/infrastructure/repositories/ChatSessionRepository.ts- Extractedsrc/infrastructure/repositories/ChatMessageRepository.ts- Extractedsrc/infrastructure/repositories/RAGRepository.ts- Extracted
HttpClient pattern:
export class HttpClient {
constructor(private apiService?: APIService) {}
async get<T>(url: string): Promise<T> {
if (this.apiService) {
// Use BrainDrive service (preferred)
return this.apiService.get<T>(url);
} else {
// Fallback to raw fetch (standalone mode)
const response = await fetch(url, {
signal: AbortSignal.timeout(10000)
});
return response.json();
}
}
// ... post, put, delete methods
}
Migration status (as of Phase 5):
- ✅ PluginService: Migrated to specialized repos
- ✅ CollectionChatService: Still uses DataRepository facade
- ✅ EvaluationService: Uses specialized repos
- ❌ Some components: Still use DataRepository
Current usage count:
- DataRepository still injected in ~10 components
- Direct repo usage: ~5 components
- Target: 0 DataRepository usages (remove facade entirely)
Deprecation timeline:
- Phase 1 (Complete): Extract repos, create facade
- Phase 2-6 (In progress): Gradually migrate components
- Phase 7-8 (Planned): Migrate remaining components
- Phase 11 (Future): Remove facade entirely
Critical gotchas:
- HttpClient fallback only has 10s timeout on GET (not POST/PUT/DELETE)
- Both paths must be tested until facade removed
- Services prop-drilling DataRepository vs injecting repos directly
- Facade methods must stay in sync with repo signatures (temp burden)
Migration pattern for components:
// OLD (facade)
constructor(private dataRepository: DataRepository) {}
async loadCollections() {
return this.dataRepository.getCollections();
}
// NEW (specialized repos)
constructor(
private collectionRepo: CollectionRepository,
private documentRepo: DocumentRepository
) {}
async loadCollections() {
return this.collectionRepo.getCollections();
}
Removal criteria:
- All components use specialized repos directly
- Zero grep results for
dataRepository.get*patterns - All tests updated to use repos
- Documentation updated (FOR-AI-CODING-AGENTS.md, AI-AGENT-GUIDE.md)
Rollback plan: Not applicable - facade will remain until all components migrated. If migration fails, facade continues working indefinitely (no breaking change).