This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / docs / url-metadata-strategy.md
10 kB 277 lines
1# URL Metadata Strategy (DDD) 2 3This document outlines the domain-driven approach for fetching, aggregating, and caching URL metadata from multiple external sources. 4 5## Problem Statement 6 7We need to: 8 91. Fetch metadata from multiple sources (Citoid, Iframely, etc.) 102. Cache results to avoid redundant API calls 113. Aggregate data from different sources intelligently 124. Maintain clean domain boundaries and testability 13 14## Domain Model 15 16### Value Objects 17 18**UrlMetadata** 19 20- Immutable value object containing normalized metadata fields 21- Derived from raw API responses through transformation 22- Used by the domain layer for business logic 23 24**RawMetadataResponse** 25 26- Immutable value object containing the original API response 27- Includes `source` field to track which service provided the data 28- Includes `retrievedAt` timestamp for cache invalidation 29- Preserves the exact JSON structure returned by each API 30 31**MetadataSource** 32 33- Enumeration of available metadata sources (CITOID, IFRAMELY, etc.) 34 35### Domain Services 36 37**MetadataAggregationService** 38 39- Coordinates fetching from multiple sources 40- Implements intelligent merging strategies 41- Handles fallback logic when sources fail 42 43### Infrastructure Services 44 45**IMetadataProvider** (Interface) 46 47- Contract for individual metadata sources 48- Returns raw API responses wrapped in RawMetadataResponse 49- Implemented by CitoidMetadataService, IframelyMetadataService, etc. 50 51**IRawMetadataRepository** (Interface) 52 53- Stores and retrieves raw API responses by URL and source 54- Enables querying for existing raw data to determine what's missing 55- Preserves original API response structure for future reprocessing 56 57**IMetadataTransformer** (Interface) 58 59- Transforms raw API responses into domain UrlMetadata objects 60- Source-specific implementations handle different API response formats 61- Enables reprocessing of stored raw data as domain models evolve 62 63## Architecture Pattern: Strategy + Repository + Aggregation 64 65``` 66┌─────────────────────────────────────────────────────────────┐ 67│ Application Layer │ 68│ GetUrlMetadataUseCase │ 69└─────────────────────┬───────────────────────────────────────┘ 70 71┌─────────────────────▼───────────────────────────────────────┐ 72│ Domain Layer │ 73│ MetadataAggregationService │ 74│ ├── IMetadataRepository (cache check/store) │ 75│ ├── IMetadataProvider[] (multiple sources) │ 76│ └── Aggregation Logic │ 77└─────────────────────┬───────────────────────────────────────┘ 78 79┌─────────────────────▼───────────────────────────────────────┐ 80│ Infrastructure Layer │ 81│ ├── DrizzleMetadataRepository │ 82│ ├── CitoidMetadataService │ 83│ ├── IframelyMetadataService │ 84│ └── OpenGraphMetadataService │ 85└─────────────────────────────────────────────────────────────┘ 86``` 87 88## Implementation Strategy 89 90### 1. Provider Interface 91 92```typescript 93export interface IMetadataProvider { 94 readonly source: MetadataSource; 95 fetchRawMetadata(url: URL): Promise<Result<RawMetadataResponse>>; 96 isAvailable(): Promise<boolean>; 97} 98``` 99 100### 2. Repository Interface 101 102```typescript 103export interface IRawMetadataRepository { 104 findByUrl(url: URL): Promise<Result<RawMetadataResponse[]>>; // All cached raw responses for URL 105 findByUrlAndSource( 106 url: URL, 107 source: MetadataSource, 108 ): Promise<Result<RawMetadataResponse | null>>; 109 save(rawResponse: RawMetadataResponse): Promise<Result<void>>; 110 isStale(rawResponse: RawMetadataResponse, maxAge: Duration): boolean; 111} 112``` 113 114### 3. Transformer Interface 115 116```typescript 117export interface IMetadataTransformer { 118 readonly source: MetadataSource; 119 transform(rawResponse: RawMetadataResponse): Result<UrlMetadata>; 120} 121``` 122 123### 4. Aggregation Service 124 125```typescript 126export class MetadataAggregationService { 127 constructor( 128 private readonly rawRepository: IRawMetadataRepository, 129 private readonly providers: IMetadataProvider[], 130 private readonly transformers: Map<MetadataSource, IMetadataTransformer>, 131 private readonly maxCacheAge: Duration = Duration.days(7), 132 ) {} 133 134 async getMetadata( 135 url: URL, 136 sources?: MetadataSource[], 137 ): Promise<Result<UrlMetadata>> { 138 // 1. Check cache for existing raw responses 139 const cachedRaw = await this.rawRepository.findByUrl(url); 140 141 // 2. Determine which sources need fresh data 142 const sourcesToFetch = this.determineSourcesToFetch(cachedRaw, sources); 143 144 // 3. Fetch raw responses from required sources in parallel 145 const freshRawResults = await this.fetchRawFromSources(url, sourcesToFetch); 146 147 // 4. Cache new raw results 148 await this.cacheRawResults(freshRawResults); 149 150 // 5. Transform all available raw responses to domain objects 151 const allRawResponses = [ 152 ...(cachedRaw.isOk() ? cachedRaw.value : []), 153 ...freshRawResults, 154 ]; 155 const transformedMetadata = this.transformRawResponses(allRawResponses); 156 157 // 6. Aggregate transformed metadata 158 return this.aggregateMetadata(transformedMetadata); 159 } 160 161 private transformRawResponses( 162 rawResponses: RawMetadataResponse[], 163 ): UrlMetadata[] { 164 return rawResponses 165 .map((raw) => { 166 const transformer = this.transformers.get(raw.source); 167 return transformer ? transformer.transform(raw) : null; 168 }) 169 .filter((result) => result?.isOk()) 170 .map((result) => result!.value); 171 } 172 173 private aggregateMetadata(metadataList: UrlMetadata[]): Result<UrlMetadata> { 174 // Intelligent merging logic: 175 // - Prefer academic sources (Citoid) for scholarly content 176 // - Prefer social media optimized sources (Iframely) for rich media 177 // - Combine fields from multiple sources (e.g., best title, description, image) 178 } 179} 180``` 181 182### 5. Use Case 183 184```typescript 185export class GetUrlMetadataUseCase { 186 constructor( 187 private readonly aggregationService: MetadataAggregationService, 188 ) {} 189 190 async execute( 191 url: string, 192 preferredSources?: MetadataSource[], 193 ): Promise<Result<UrlMetadata>> { 194 const urlResult = URL.create(url); 195 if (urlResult.isErr()) { 196 return err(urlResult.error); 197 } 198 199 return this.aggregationService.getMetadata( 200 urlResult.value, 201 preferredSources, 202 ); 203 } 204} 205``` 206 207## Caching Strategy 208 209### Cache Key Structure 210 211- Raw responses: `raw_metadata:{url_hash}:{source}` 212- Transformed metadata: `url_metadata:{url_hash}` (optional, for performance) 213 214### Cache Invalidation 215 216- Time-based: 7 days default, configurable per source 217- Manual: When user requests fresh metadata 218- Source-specific: Different TTL for different providers 219 220### Partial Cache Hits 221 222- If we have Citoid data but need Iframely data, only fetch from Iframely 223- Aggregate cached + fresh data intelligently 224 225## Data Aggregation Rules 226 227### Field Priority (Configurable) 228 2291. **Title**: Citoid > Iframely > OpenGraph 2302. **Description**: Iframely > Citoid > OpenGraph 2313. **Author**: Citoid > Iframely 2324. **Image**: Iframely > OpenGraph > Citoid 2335. **Published Date**: Citoid > Iframely 234 235### Conflict Resolution 236 237- Prefer more recent data when timestamps differ significantly 238- Prefer more complete data (fewer null fields) 239- Allow manual source preference overrides 240 241## Benefits of This Approach 242 2431. **Domain Purity**: Business logic stays in domain layer 2442. **Testability**: Easy to mock providers and repository 2453. **Flexibility**: Easy to add new metadata sources 2464. **Performance**: Intelligent caching reduces API calls 2475. **Reliability**: Fallback between sources when one fails 2486. **Configurability**: Source preferences can be adjusted per use case 249 250## Future Enhancements 251 2521. **Source Health Monitoring**: Track success rates and response times 2532. **Dynamic Source Selection**: Choose sources based on URL patterns 2543. **Batch Processing**: Fetch metadata for multiple URLs efficiently 2554. **User Preferences**: Allow users to prefer certain sources 2565. **Metadata Enrichment**: Combine multiple sources for richer data 257 258## Raw Data Storage Benefits 259 2601. **Data Preservation**: Original API responses preserved exactly as returned 2612. **Reprocessing Capability**: Can re-transform data as domain models evolve 2623. **Debugging**: Easy to inspect what each API actually returned 2634. **Audit Trail**: Complete history of API interactions 2645. **Schema Evolution**: Domain objects can change without losing source data 2656. **Multi-version Support**: Can support multiple versions of transformers 266 267## Implementation Order 268 2691. Define interfaces (`IMetadataProvider`, `IRawMetadataRepository`, `IMetadataTransformer`) 2702. Implement raw metadata repository with caching 2713. Create `RawMetadataResponse` value object 2724. Refactor existing `CitoidMetadataService` to return raw responses 2735. Implement `CitoidMetadataTransformer` to convert raw to domain objects 2746. Implement `MetadataAggregationService` 2757. Add additional providers and transformers (Iframely, OpenGraph) 2768. Implement intelligent aggregation logic 2779. Add monitoring and health checks