A social knowledge tool for researchers built on ATProto
1# User Data Enrichment in DDD 2 3This document outlines Domain-Driven Design (DDD) patterns for handling data enrichment from external services, particularly for user profile data that needs to be combined with core domain data. 4 5## The Problem 6 7When displaying collections, cards, or other domain entities, we often need to show rich user information (names, avatars, etc.) that comes from a different bounded context or external service. The challenge is how to combine this data while maintaining clean domain boundaries. 8 9## DDD Patterns for Data Enrichment 10 11### 1. Application Service Orchestration (Recommended) 12 13This is the most common DDD pattern for cross-cutting concerns like user profile enrichment: 14 15```typescript 16// Application Service coordinates between bounded contexts 17export class CollectionApplicationService { 18 constructor( 19 private getMyCollectionsUseCase: GetMyCollectionsUseCase, 20 private userProfileService: IUserProfileService, // External service 21 ) {} 22 23 async getMyCollectionsWithProfiles(query: GetMyCollectionsQuery) { 24 // 1. Execute core use case 25 const collectionsResult = await this.getMyCollectionsUseCase.execute(query); 26 27 if (collectionsResult.isErr()) { 28 return collectionsResult; 29 } 30 31 // 2. Enrich with external data 32 const curatorIds = collectionsResult.value.collections.map( 33 (c) => c.createdBy.id, 34 ); 35 const profiles = await this.userProfileService.getProfiles(curatorIds); 36 37 // 3. Merge data 38 const enrichedCollections = collectionsResult.value.collections.map( 39 (collection) => ({ 40 ...collection, 41 createdBy: { 42 ...collection.createdBy, 43 ...profiles.get(collection.createdBy.id), 44 }, 45 }), 46 ); 47 48 return ok({ 49 ...collectionsResult.value, 50 collections: enrichedCollections, 51 }); 52 } 53} 54``` 55 56### 2. Domain Service for Cross-Context Integration 57 58When the enrichment is a core business concern: 59 60```typescript 61// Domain Service in the Cards bounded context 62export class CollectionEnrichmentService implements DomainService { 63 constructor( 64 private userContextGateway: IUserContextGateway, // Anti-corruption layer 65 ) {} 66 67 async enrichCollectionsWithCreatorInfo( 68 collections: Collection[], 69 ): Promise<EnrichedCollectionData[]> { 70 const creatorIds = collections.map((c) => c.authorId.value); 71 const creatorProfiles = 72 await this.userContextGateway.getCreatorProfiles(creatorIds); 73 74 return collections.map((collection) => ({ 75 collection, 76 creatorProfile: creatorProfiles.get(collection.authorId.value), 77 })); 78 } 79} 80``` 81 82### 3. Anti-Corruption Layer Pattern 83 84Protect your domain from external service changes: 85 86```typescript 87// Interface in your domain 88export interface IUserContextGateway { 89 getCreatorProfiles(userIds: string[]): Promise<Map<string, CreatorProfile>>; 90} 91 92// Domain model for external data 93export interface CreatorProfile { 94 id: string; 95 displayName: string; 96 avatarUrl?: string; 97} 98 99// Implementation that adapts external service 100export class UserContextGateway implements IUserContextGateway { 101 constructor(private externalUserService: ExternalUserAPI) {} 102 103 async getCreatorProfiles( 104 userIds: string[], 105 ): Promise<Map<string, CreatorProfile>> { 106 const externalProfiles = 107 await this.externalUserService.getUsersByIds(userIds); 108 109 // Transform external format to domain format 110 const profiles = new Map<string, CreatorProfile>(); 111 externalProfiles.forEach((profile) => { 112 profiles.set(profile.user_id, { 113 id: profile.user_id, 114 displayName: profile.full_name || profile.username, 115 avatarUrl: profile.profile_image_url, 116 }); 117 }); 118 119 return profiles; 120 } 121} 122``` 123 124### 4. Event-Driven Enrichment (For High Performance) 125 126When you need to avoid real-time calls: 127 128```typescript 129// Read model that's kept in sync via events 130export interface EnrichedCollectionReadModel { 131 id: string; 132 name: string; 133 description?: string; 134 cardCount: number; 135 createdAt: Date; 136 updatedAt: Date; 137 createdBy: { 138 id: string; 139 name: string; 140 avatarUrl?: string; 141 }; 142} 143 144// Event handler that updates read model when user profiles change 145export class UserProfileUpdatedHandler { 146 constructor( 147 private collectionReadModelRepo: ICollectionReadModelRepository, 148 ) {} 149 150 async handle(event: UserProfileUpdatedEvent) { 151 // Update all collections created by this user 152 await this.collectionReadModelRepo.updateCreatorInfo(event.userId, { 153 name: event.newDisplayName, 154 avatarUrl: event.newAvatarUrl, 155 }); 156 } 157} 158``` 159 160## Current Implementation Analysis 161 162Our current approach with `ICuratorEnrichmentService` follows good DDD principles: 163 164```typescript 165// This is a good application-level service 166export interface ICuratorEnrichmentService { 167 enrichCurators(curatorIds: string[]): Promise<Map<string, CuratorInfo>>; 168} 169``` 170 171**Strengths:** 172 173- ✅ Keeps enrichment logic out of the domain 174- ✅ Single responsibility for user data enrichment 175- ✅ Easy to test and mock 176- ✅ Follows dependency inversion 177 178## Recommended Improvements 179 180### 1. Add Anti-Corruption Layer 181 182```typescript 183export class CuratorEnrichmentService implements ICuratorEnrichmentService { 184 constructor(private userContextGateway: IUserContextGateway) {} 185 186 async enrichCurators( 187 curatorIds: string[], 188 ): Promise<Map<string, CuratorInfo>> { 189 try { 190 return await this.userContextGateway.getCreatorProfiles(curatorIds); 191 } catch (error) { 192 // Graceful degradation - return minimal info 193 const fallbackMap = new Map<string, CuratorInfo>(); 194 curatorIds.forEach((id) => { 195 fallbackMap.set(id, { id, name: 'Unknown User' }); 196 }); 197 return fallbackMap; 198 } 199 } 200} 201``` 202 203### 2. Add Caching for Performance 204 205```typescript 206export class CachedCuratorEnrichmentService 207 implements ICuratorEnrichmentService 208{ 209 constructor( 210 private userContextGateway: IUserContextGateway, 211 private cache: ICache, 212 ) {} 213 214 async enrichCurators( 215 curatorIds: string[], 216 ): Promise<Map<string, CuratorInfo>> { 217 const cached = await this.cache.getMany(curatorIds); 218 const uncachedIds = curatorIds.filter((id) => !cached.has(id)); 219 220 if (uncachedIds.length > 0) { 221 const fresh = 222 await this.userContextGateway.getCreatorProfiles(uncachedIds); 223 await this.cache.setMany(fresh, { ttl: 300 }); // 5 min cache 224 225 // Merge cached and fresh data 226 fresh.forEach((value, key) => cached.set(key, value)); 227 } 228 229 return cached; 230 } 231} 232``` 233 234### 3. Move to Application Service (Optional) 235 236If you want to keep the use case purely focused on the core domain: 237 238```typescript 239export class CollectionQueryApplicationService { 240 constructor( 241 private getMyCollectionsUseCase: GetMyCollectionsUseCase, 242 private curatorEnrichmentService: ICuratorEnrichmentService, 243 ) {} 244 245 async getMyCollectionsWithProfiles(query: GetMyCollectionsQuery) { 246 // Execute core use case (returns minimal curator data) 247 const result = await this.getMyCollectionsUseCase.execute(query); 248 249 if (result.isErr()) return result; 250 251 // Enrich at application service level 252 const curatorIds = result.value.collections.map((c) => c.authorId); 253 const enrichedCurators = 254 await this.curatorEnrichmentService.enrichCurators(curatorIds); 255 256 // Transform to enriched DTOs 257 const enrichedCollections = result.value.collections.map((collection) => ({ 258 ...collection, 259 createdBy: enrichedCurators.get(collection.authorId) || { 260 id: collection.authorId, 261 name: 'Unknown User', 262 }, 263 })); 264 265 return ok({ 266 ...result.value, 267 collections: enrichedCollections, 268 }); 269 } 270} 271``` 272 273## Best Practices 274 2751. **Separate Concerns**: Keep domain logic separate from external data enrichment 2762. **Graceful Degradation**: Always have fallbacks when external services fail 2773. **Performance**: Consider caching for frequently accessed user data 2784. **Anti-Corruption**: Protect your domain from external service changes 2795. **Testability**: Make enrichment services easy to mock and test 2806. **Consistency**: Use the same enrichment patterns across your application 281 282## When to Use Each Pattern 283 284- **Application Service Orchestration**: Most common case, good default choice 285- **Domain Service**: When enrichment is core business logic 286- **Anti-Corruption Layer**: When external services are unstable or have different models 287- **Event-Driven**: When you need high performance and can tolerate eventual consistency 288 289Our current implementation is solid and follows DDD principles well. The main opportunities for improvement are adding resilience (graceful degradation) and performance optimizations (caching).