A social knowledge tool for researchers built on ATProto
1# Event System Architecture 2 3This document outlines the event system architecture for our DDD-based application, building on the existing `AggregateRoot` and `DomainEvents` abstractions. 4 5## Overview 6 7Our event system follows Domain-Driven Design principles where domain events represent meaningful business occurrences that domain experts care about. Events are raised by aggregate roots when their state changes in significant ways. 8 9## Core Abstractions (Already Implemented) 10 11### IDomainEvent Interface 12 13```typescript 14export interface IDomainEvent { 15 dateTimeOccurred: Date; 16 getAggregateId(): UniqueEntityID; 17} 18``` 19 20### AggregateRoot Base Class 21 22- Maintains a list of domain events (`_domainEvents`) 23- Provides `addDomainEvent()` method to raise events 24- Automatically marks aggregate for dispatch via `DomainEvents.markAggregateForDispatch()` 25- Provides `clearEvents()` to clear events after dispatch 26 27### DomainEvents Static Class 28 29- Manages event handlers registration 30- Tracks aggregates that have raised events 31- Dispatches events when `dispatchEventsForAggregate()` is called 32- Provides handler registration via `register(callback, eventClassName)` 33 34## Event System Architecture Layers 35 36### 1. Domain Layer - Event Definitions 37 38**Location**: `src/modules/{module}/domain/events/` 39 40Domain events should be defined as classes that implement `IDomainEvent`. They represent pure business concepts. 41 42```typescript 43// Example: CardAddedToLibraryEvent 44export class CardAddedToLibraryEvent implements IDomainEvent { 45 public readonly dateTimeOccurred: Date; 46 47 constructor( 48 public readonly cardId: CardId, 49 public readonly curatorId: CuratorId, 50 public readonly cardType: CardTypeEnum, 51 public readonly url?: string, 52 ) { 53 this.dateTimeOccurred = new Date(); 54 } 55 56 getAggregateId(): UniqueEntityID { 57 return this.cardId.getValue(); 58 } 59} 60``` 61 62**Event Naming Conventions**: 63 64- Use past tense: `CardAddedToLibraryEvent`, `CollectionCreatedEvent` 65- Be specific about the business action: `UrlCardAddedToLibraryEvent` vs generic `CardUpdatedEvent` 66- Include relevant context data needed by event handlers 67 68### 2. Domain Layer - Raising Events in Aggregates 69 70Events should be raised within aggregate methods when business-significant state changes occur: 71 72```typescript 73// In Card aggregate 74export class Card extends AggregateRoot<CardProps> { 75 public addToLibrary(curatorId: CuratorId): Result<void> { 76 // Business logic validation 77 if (this.isInLibrary(curatorId)) { 78 return err(new Error('Card already in library')); 79 } 80 81 // Apply state change 82 this.props.libraries.push(curatorId); 83 84 // Raise domain event 85 this.addDomainEvent( 86 new CardAddedToLibraryEvent( 87 this.cardId, 88 curatorId, 89 this.props.type.value, 90 this.props.content.url?.value, 91 ), 92 ); 93 94 return ok(); 95 } 96} 97``` 98 99**When to Raise Events**: 100 101- After successful state changes, not before 102- For business-significant actions that other bounded contexts care about 103- When side effects or integrations need to be triggered 104- For audit trails and business intelligence 105 106### 3. Application Layer - Event Handlers 107 108**Location**: `src/modules/{module}/application/eventHandlers/` 109 110Event handlers contain application logic that should happen in response to domain events. They coordinate between different services and bounded contexts. 111 112```typescript 113// Example: CardAddedToLibraryEventHandler 114export class CardAddedToLibraryEventHandler { 115 constructor( 116 private profileService: IProfileService, 117 private notificationService: INotificationService, 118 private analyticsService: IAnalyticsService, 119 ) {} 120 121 async handle(event: CardAddedToLibraryEvent): Promise<void> { 122 try { 123 // Update user activity metrics 124 await this.analyticsService.trackCardAddedToLibrary({ 125 cardId: event.cardId.getStringValue(), 126 curatorId: event.curatorId.value, 127 cardType: event.cardType, 128 timestamp: event.dateTimeOccurred, 129 }); 130 131 // Send notification to followers (if public library) 132 const profile = await this.profileService.getProfile( 133 event.curatorId.value, 134 ); 135 if (profile.isSuccess() && profile.value.isPublic) { 136 await this.notificationService.notifyFollowers({ 137 userId: event.curatorId.value, 138 action: 'added_card_to_library', 139 cardId: event.cardId.getStringValue(), 140 }); 141 } 142 143 // Trigger content indexing for search 144 if (event.url) { 145 await this.searchIndexService.indexCard({ 146 cardId: event.cardId.getStringValue(), 147 url: event.url, 148 curatorId: event.curatorId.value, 149 }); 150 } 151 } catch (error) { 152 // Log error but don't fail the main operation 153 console.error('Error handling CardAddedToLibraryEvent:', error); 154 } 155 } 156} 157``` 158 159**Event Handler Principles**: 160 161- Handlers should be idempotent (safe to run multiple times) 162- Failures in handlers should not fail the main business operation 163- Handlers can coordinate multiple services but shouldn't contain business logic 164- Use dependency injection for testability 165 166### 4. Infrastructure Layer - Event Handler Registration 167 168**Location**: `src/shared/infrastructure/events/` 169 170Create an event handler registry that wires up domain events to their handlers: 171 172```typescript 173// EventHandlerRegistry 174export class EventHandlerRegistry { 175 constructor( 176 private cardAddedToLibraryHandler: CardAddedToLibraryEventHandler, 177 private collectionCreatedHandler: CollectionCreatedEventHandler, 178 // ... other handlers 179 ) {} 180 181 registerAllHandlers(): void { 182 // Register card events 183 DomainEvents.register( 184 (event: CardAddedToLibraryEvent) => 185 this.cardAddedToLibraryHandler.handle(event), 186 CardAddedToLibraryEvent.name, 187 ); 188 189 DomainEvents.register( 190 (event: CollectionCreatedEvent) => 191 this.collectionCreatedHandler.handle(event), 192 CollectionCreatedEvent.name, 193 ); 194 195 // ... register other event handlers 196 } 197 198 clearAllHandlers(): void { 199 DomainEvents.clearHandlers(); 200 } 201} 202``` 203 204### 5. Infrastructure Layer - Event Dispatch Integration 205 206Events should be dispatched at the end of successful use case executions, typically in a repository save operation or use case completion: 207 208```typescript 209// In repository implementation 210export class DrizzleCardRepository implements ICardRepository { 211 async save(card: Card): Promise<Result<void>> { 212 try { 213 // Save to database 214 await this.db.insert(cards).values(this.toPersistence(card)); 215 216 // Dispatch events after successful save 217 DomainEvents.dispatchEventsForAggregate(card.id); 218 219 return ok(); 220 } catch (error) { 221 return err(error); 222 } 223 } 224} 225``` 226 227**Alternative**: Dispatch in use case after repository operations: 228 229```typescript 230// In use case 231export class AddUrlToLibraryUseCase { 232 async execute( 233 request: AddUrlToLibraryDTO, 234 ): Promise<Result<AddUrlToLibraryResponseDTO>> { 235 try { 236 // ... business logic 237 238 const saveResult = await this.cardRepository.save(urlCard); 239 if (saveResult.isErr()) { 240 return err(saveResult.error); 241 } 242 243 // Dispatch events after successful persistence 244 DomainEvents.dispatchEventsForAggregate(urlCard.id); 245 246 return ok(response); 247 } catch (error) { 248 return err(AppError.UnexpectedError.create(error)); 249 } 250 } 251} 252``` 253 254## Event System Integration Points 255 256### Factory Registration 257 258Event handlers need to be registered in the factory system: 259 260```typescript 261// In ServiceFactory 262export class ServiceFactory { 263 static create( 264 configService: EnvironmentConfigService, 265 repositories: Repositories, 266 ): Services { 267 // ... existing services 268 269 // Event handlers 270 const cardAddedToLibraryHandler = new CardAddedToLibraryEventHandler( 271 profileService, 272 notificationService, 273 analyticsService, 274 ); 275 276 const eventHandlerRegistry = new EventHandlerRegistry( 277 cardAddedToLibraryHandler, 278 // ... other handlers 279 ); 280 281 // Register all event handlers 282 eventHandlerRegistry.registerAllHandlers(); 283 284 return { 285 // ... existing services 286 eventHandlerRegistry, 287 }; 288 } 289} 290``` 291 292### Testing Considerations 293 294For testing, you'll want to: 295 2961. **Clear events between tests**: 297 298```typescript 299afterEach(() => { 300 DomainEvents.clearHandlers(); 301 DomainEvents.clearMarkedAggregates(); 302}); 303``` 304 3052. **Test event raising**: 306 307```typescript 308it('should raise CardAddedToLibraryEvent when card is added to library', () => { 309 const card = CardFactory.create(cardInput); 310 const curatorId = CuratorId.create('did:plc:test'); 311 312 card.addToLibrary(curatorId); 313 314 expect(card.domainEvents).toHaveLength(1); 315 expect(card.domainEvents[0]).toBeInstanceOf(CardAddedToLibraryEvent); 316}); 317``` 318 3193. **Test event handlers in isolation**: 320 321```typescript 322it('should track analytics when card is added to library', async () => { 323 const handler = new CardAddedToLibraryEventHandler( 324 mockProfileService, 325 mockNotificationService, 326 mockAnalyticsService, 327 ); 328 329 const event = new CardAddedToLibraryEvent( 330 cardId, 331 curatorId, 332 CardTypeEnum.URL, 333 'https://example.com', 334 ); 335 336 await handler.handle(event); 337 338 expect(mockAnalyticsService.trackCardAddedToLibrary).toHaveBeenCalledWith({ 339 cardId: cardId.getStringValue(), 340 curatorId: curatorId.value, 341 cardType: CardTypeEnum.URL, 342 timestamp: event.dateTimeOccurred, 343 }); 344}); 345``` 346 347## Event Design Guidelines 348 349### Event Granularity 350 351- **Too Fine**: `CardTitleChangedEvent`, `CardDescriptionChangedEvent` (unless specifically needed) 352- **Too Coarse**: `CardUpdatedEvent` (not specific enough) 353- **Just Right**: `CardAddedToLibraryEvent`, `CardRemovedFromCollectionEvent` 354 355### Event Data 356 357- Include the aggregate ID that raised the event 358- Include enough context for handlers to do their work 359- Don't include entire aggregate state (events should be lightweight) 360- Include timestamp for ordering and auditing 361 362### Event Naming 363 364- Use past tense (events represent things that have happened) 365- Be specific about the business action 366- Include the aggregate type: `Card...Event`, `Collection...Event` 367 368### Handler Design 369 370- Keep handlers focused on a single responsibility 371- Make handlers idempotent 372- Don't let handler failures break the main business operation 373- Use async/await for I/O operations 374- Log errors appropriately 375 376## Example Event Scenarios 377 378### URL Card Added to Library 379 3801. **Trigger**: `Card.addToLibrary()` method called 3812. **Event**: `CardAddedToLibraryEvent` raised 3823. **Handlers**: 383 - Update user activity metrics 384 - Notify followers 385 - Index for search 386 - Update recommendation engine 387 388### Collection Created 389 3901. **Trigger**: `Collection.create()` factory method 3912. **Event**: `CollectionCreatedEvent` raised 3923. **Handlers**: 393 - Send welcome notification 394 - Update user profile stats 395 - Trigger initial recommendations 396 397### Card Removed from Library 398 3991. **Trigger**: `Card.removeFromLibrary()` method 4002. **Event**: `CardRemovedFromLibraryEvent` raised 4013. **Handlers**: 402 - Update analytics 403 - Remove from search index 404 - Clean up recommendations 405 406## useful context 407 408``` 409docs/event-system-architecture.md 410docs/features/GUIDE.md 411src/modules/cards/application/useCases/commands/AddUrlToLibraryUseCase.ts 412src/modules/cards/domain/Card.ts 413src/modules/cards/domain/events/CardAddedToLibraryEvent.ts 414src/modules/cards/infrastructure/http/controllers/AddUrlToLibraryController.ts 415src/modules/feeds/application/eventHandlers/CardAddedToLibraryEventHandler.ts 416src/modules/feeds/application/ports/IFeedService.ts 417src/modules/notifications/application/eventHandlers/CardAddedToLibraryEventHandler.ts 418src/modules/notifications/application/ports/INotificationService.ts 419src/shared/domain/AggregateRoot.ts 420src/shared/domain/events/DomainEvents.ts 421src/shared/domain/events/IDomainEvent.ts 422src/shared/infrastructure/events/EventHandlerRegistry.ts 423```