Skip to content

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:

  1. Complex Dependency Management: The current architecture has intricate dependencies between components, registries, adapters, and processors, making it difficult to maintain and extend.

  2. Inconsistent Interface Implementations: Multiple implementations of ContentAdapter and ContentRegistry interfaces have diverged, leading to inconsistent behavior and API mismatches.

  3. Type Safety Issues: The system has numerous TypeScript errors related to incompatible types, missing methods, and parameter mismatches.

  4. Feature Discovery Complexity: Components need to check for optional methods and features, creating defensive programming patterns throughout the codebase.

  5. 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:

  1. Registry as Facade: Transform the Registry from a content store to a facade that orchestrates all content operations, providing a clean API for components.

  2. Loader as Strategy Provider: Create a Loader layer that selects appropriate processing and adapter strategies based on content type and path.

  3. Pluggable Processor System: Implement a registry for processors that can be extended at runtime, with support for multiple processors per content type.

  4. Adapter Provider Pattern: Create a registry for adapters with providers that can create appropriate adapters based on content path and options.

  5. 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

  1. 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
  2. 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
  3. 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

  1. Clearer Separation of Concerns: Each layer has specific responsibilities with well-defined boundaries
  2. Simplified API Surface: Components only need to interact with the Registry facade
  3. Better Extensibility: New content types, processors, and adapters can be added without changing existing code
  4. Enhanced Testability: Each layer can be tested in isolation with mock implementations
  5. Consistent Error Handling: Standardized error handling patterns throughout the system
  6. Runtime Extension: Support for plugins and extensions at runtime

Challenges

  1. Migration Complexity: Moving from the current architecture requires careful planning
  2. Performance Considerations: Additional layers could impact performance if not carefully implemented
  3. Learning Curve: More sophisticated architecture requires better documentation and examples
  4. Backward Compatibility: Ensuring existing code continues to work during migration

Implementation

The implementation will follow a phased approach:

Phase 1: Core Interfaces

typescript
// 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

typescript
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

  1. Create new interfaces without changing existing code
  2. Implement the Loader as a new layer that uses existing components
  3. Refactor the Registry to use the Loader instead of direct implementation
  4. Update React components to use the new Registry API
  5. Refactor Adapters and Processors to implement the new interfaces

References

Released under the MIT License.