ADR-003: Registry Facade Architecture
Status
Superseded by ADR-004 | Proposed | Accepted | Implemented
Version History
- v1: 2025-04-01 - Initial proposal
Context
Our current content system has evolved with several architectural challenges:
Complex Dependency Management: The current architecture has intricate dependencies between components, registries, adapters, and processors, making it difficult to maintain and extend.
Inconsistent Interface Implementations: Multiple implementations of
ContentAdapter
andContentRegistry
interfaces have diverged, leading to inconsistent behavior and API mismatches.Type Safety Issues: The system has numerous TypeScript errors related to incompatible types, missing methods, and parameter mismatches.
Feature Discovery Complexity: Components need to check for optional methods and features, creating defensive programming patterns throughout the codebase.
Testing Challenges: The current architecture makes isolation testing difficult due to tight coupling between components.
The need for a cleaner architecture that addresses these issues while enabling new features like real-time collaboration, AI integration, and enhanced content authoring has become apparent.
Decision
Implement a new architecture with a strict top-down approach:
React Components → Registry → Loader → (Processor | Adapter)
Key changes include:
Registry as Facade: Transform the Registry from a content store to a facade that orchestrates all content operations, providing a clean API for components.
Loader as Strategy Provider: Create a Loader layer that selects appropriate processing and adapter strategies based on content type and path.
Pluggable Processor System: Implement a registry for processors that can be extended at runtime, with support for multiple processors per content type.
Adapter Provider Pattern: Create a registry for adapters with providers that can create appropriate adapters based on content path and options.
React Integration with Hook Factory: Develop a hook factory that creates custom hooks for registry interaction, ensuring consistent patterns.
This approach creates a clear dependency flow, with each layer having specific responsibilities and a well-defined API.
Alternatives Considered
Incremental Refactoring of Existing Architecture:
- Pros: Less disruptive, can be done in smaller pieces
- Cons: Doesn’t address fundamental architectural issues, complexity would remain
- Outcome: Rejected because architectural issues require structural changes
Microservices Approach with API Gateway:
- Pros: Clear separation of concerns, independent scaling
- Cons: Excessive complexity for current needs, performance overhead
- Outcome: Rejected as overly complex for a content system that primarily runs in the browser
Event-Driven Architecture:
- Pros: Looser coupling, reactive programming model
- Cons: Increased complexity, harder to reason about data flow
- Outcome: Partially adopted: Content watching uses an event-like pattern, but full event-driven architecture was deemed unnecessary
Consequences
What are the consequences of this decision:
Benefits
- Clearer Separation of Concerns: Each layer has specific responsibilities with well-defined boundaries
- Simplified API Surface: Components only need to interact with the Registry facade
- Better Extensibility: New content types, processors, and adapters can be added without changing existing code
- Enhanced Testability: Each layer can be tested in isolation with mock implementations
- Consistent Error Handling: Standardized error handling patterns throughout the system
- Runtime Extension: Support for plugins and extensions at runtime
Challenges
- Migration Complexity: Moving from the current architecture requires careful planning
- Performance Considerations: Additional layers could impact performance if not carefully implemented
- Learning Curve: More sophisticated architecture requires better documentation and examples
- Backward Compatibility: Ensuring existing code continues to work during migration
Implementation
The implementation will follow a phased approach:
Phase 1: Core Interfaces
// Registry Facade
export interface ContentRegistry {
// Core content operations
getContent(path: string, options?: ContentOptions): Promise<MdxData>
listContent(type?: ContentType, options?: ListOptions): Promise<ContentList>
watchContent(path: string, callback: ContentCallback): Unsubscribe
// Registry capabilities
hasContent(path: string): Promise<boolean>
getCapabilities(): RegistryCapabilities
}
// Loader Strategy Provider
export class ContentLoader {
constructor(
private processorRegistry: ProcessorRegistry,
private adapterRegistry: AdapterRegistry
) {}
async loadContent(path: string, options?: ContentOptions): Promise<MdxData> {
// Implementation details
}
}
// Processor Registry
export interface ContentProcessor {
supportedTypes: ContentMediaType[]
process(content: RawContent, options?: ProcessorOptions): Promise<MdxData>
canProcess(content: RawContent): boolean
}
// Adapter Registry
export interface AdapterProvider {
canHandle(path: string, options?: ContentOptions): boolean
createAdapter(path: string, options?: ContentOptions): ContentAdapter
priority: number
}
Phase 2: React Integration
export function createContentHooks(registry: ContentRegistry) {
return {
useContent(path: string, options?: ContentOptions) {
// Implementation using React hooks
},
// Additional hooks for listing, watching, etc.
}
}
Phase 3: Migration Strategy
- Create new interfaces without changing existing code
- Implement the Loader as a new layer that uses existing components
- Refactor the Registry to use the Loader instead of direct implementation
- Update React components to use the new Registry API
- Refactor Adapters and Processors to implement the new interfaces