alpha
Login
or
Join now
nandi.uk
/
semble
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
This repository has no description
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
domain event processing guide
author
Wesley Finck
date
6 months ago
(Jan 23, 2026, 3:47 PM -0800)
commit
e645a31e
e645a31e5403e34232741eb9afdbbce56fa23f44
parent
70001ba4
70001ba48ca4fcb610b8c4c16b6238f5c570aa6e
+327
1 changed file
Expand all
Collapse all
Unified
Split
.agent
logs
20260123_domain_event_processing.md
+327
.agent/logs/20260123_domain_event_processing.md
View file
Reviewed
···
1
1
+
# Domain Events Guide
2
2
+
3
3
+
This document summarizes how domain events are structured, published, and handled in the codebase, along with a checklist for adding new events.
4
4
+
5
5
+
---
6
6
+
7
7
+
## Architecture Overview
8
8
+
9
9
+
The event system uses **BullMQ** (Redis-backed queues) for asynchronous event processing. Events flow through these layers:
10
10
+
11
11
+
```
12
12
+
Domain Entity → Use Case → Event Publisher → Redis Queue → Worker → Event Handler/Saga
13
13
+
```
14
14
+
15
15
+
---
16
16
+
17
17
+
## Key Files & Components
18
18
+
19
19
+
| Component | Location | Purpose |
20
20
+
| -------------------- | ------------------------------------------------------------------ | ------------------------------------------------------ |
21
21
+
| Event Names Registry | `src/shared/infrastructure/events/EventConfig.ts` | Central registry of all event type names |
22
22
+
| Event Publisher | `src/shared/infrastructure/events/BullMQEventPublisher.ts` | Routes events to appropriate queues |
23
23
+
| Event Subscriber | `src/shared/infrastructure/events/BullMQEventSubscriber.ts` | Consumes events from queues and dispatches to handlers |
24
24
+
| Event Mapper | `src/shared/infrastructure/events/EventMapper.ts` | Serializes/deserializes events for transport |
25
25
+
| Queue Config | `src/shared/infrastructure/events/QueueConfig.ts` | Queue names and options |
26
26
+
| Worker Process | `src/shared/infrastructure/processes/NotificationWorkerProcess.ts` | Initializes subscribers and registers handlers |
27
27
+
28
28
+
---
29
29
+
30
30
+
## Checklist: Adding a New Domain Event
31
31
+
32
32
+
### 1. Register the Event Name
33
33
+
34
34
+
**File:** `src/shared/infrastructure/events/EventConfig.ts`
35
35
+
36
36
+
```typescript
37
37
+
export const EventNames = {
38
38
+
// ... existing events
39
39
+
YOUR_NEW_EVENT: 'YourNewEventEvent',
40
40
+
} as const;
41
41
+
```
42
42
+
43
43
+
### 2. Create the Domain Event Class
44
44
+
45
45
+
**Location:** `src/modules/<module>/domain/events/YourNewEvent.ts`
46
46
+
47
47
+
Required structure:
48
48
+
49
49
+
```typescript
50
50
+
import { IDomainEvent } from '../../../../shared/domain/events/IDomainEvent';
51
51
+
import { UniqueEntityID } from '../../../../shared/domain/UniqueEntityID';
52
52
+
import { EventNames } from '../../../../shared/infrastructure/events/EventConfig';
53
53
+
import { Result, ok } from '../../../../shared/core/Result';
54
54
+
55
55
+
export class YourNewEvent implements IDomainEvent {
56
56
+
public readonly eventName = EventNames.YOUR_NEW_EVENT;
57
57
+
public readonly dateTimeOccurred: Date;
58
58
+
59
59
+
private constructor(
60
60
+
public readonly someId: SomeValueObject,
61
61
+
dateTimeOccurred?: Date,
62
62
+
) {
63
63
+
this.dateTimeOccurred = dateTimeOccurred || new Date();
64
64
+
}
65
65
+
66
66
+
public static create(someId: SomeValueObject): Result<YourNewEvent> {
67
67
+
return ok(new YourNewEvent(someId));
68
68
+
}
69
69
+
70
70
+
public static reconstruct(
71
71
+
someId: SomeValueObject,
72
72
+
dateTimeOccurred: Date,
73
73
+
): Result<YourNewEvent> {
74
74
+
return ok(new YourNewEvent(someId, dateTimeOccurred));
75
75
+
}
76
76
+
77
77
+
getAggregateId(): UniqueEntityID {
78
78
+
return this.someId.getValue();
79
79
+
}
80
80
+
}
81
81
+
```
82
82
+
83
83
+
### 3. Raise the Event in the Domain Entity
84
84
+
85
85
+
**Location:** Domain entity method (e.g., `src/modules/cards/domain/Card.ts`)
86
86
+
87
87
+
```typescript
88
88
+
import { YourNewEvent } from './events/YourNewEvent';
89
89
+
90
90
+
// Inside the domain method that triggers the event:
91
91
+
public doSomething(): Result<void> {
92
92
+
// ... business logic ...
93
93
+
94
94
+
const domainEvent = YourNewEvent.create(this.someId);
95
95
+
if (domainEvent.isErr()) {
96
96
+
return err(new ValidationError(domainEvent.error.message));
97
97
+
}
98
98
+
this.addDomainEvent(domainEvent.value);
99
99
+
100
100
+
return ok(undefined);
101
101
+
}
102
102
+
```
103
103
+
104
104
+
### 4. Update the Event Mapper
105
105
+
106
106
+
**File:** `src/shared/infrastructure/events/EventMapper.ts`
107
107
+
108
108
+
**Step 4a:** Add the import:
109
109
+
110
110
+
```typescript
111
111
+
import { YourNewEvent } from '../../../modules/<module>/domain/events/YourNewEvent';
112
112
+
```
113
113
+
114
114
+
**Step 4b:** Add the serialized interface:
115
115
+
116
116
+
```typescript
117
117
+
export interface SerializedYourNewEvent extends SerializedEvent {
118
118
+
eventType: typeof EventNames.YOUR_NEW_EVENT;
119
119
+
someId: string;
120
120
+
// ... other properties
121
121
+
}
122
122
+
```
123
123
+
124
124
+
**Step 4c:** Add to the union type:
125
125
+
126
126
+
```typescript
127
127
+
export type SerializedEventUnion =
128
128
+
| SerializedCardAddedToLibraryEvent
129
129
+
// ... other events
130
130
+
| SerializedYourNewEvent;
131
131
+
```
132
132
+
133
133
+
**Step 4d:** Add serialization in `toSerialized()`:
134
134
+
135
135
+
```typescript
136
136
+
if (event instanceof YourNewEvent) {
137
137
+
return {
138
138
+
eventType: EventNames.YOUR_NEW_EVENT,
139
139
+
aggregateId: event.getAggregateId().toString(),
140
140
+
dateTimeOccurred: event.dateTimeOccurred.toISOString(),
141
141
+
someId: event.someId.getValue().toString(),
142
142
+
};
143
143
+
}
144
144
+
```
145
145
+
146
146
+
**Step 4e:** Add deserialization in `fromSerialized()`:
147
147
+
148
148
+
```typescript
149
149
+
case EventNames.YOUR_NEW_EVENT: {
150
150
+
const someId = SomeId.createFromString(eventData.someId).unwrap();
151
151
+
const dateTimeOccurred = new Date(eventData.dateTimeOccurred);
152
152
+
return YourNewEvent.reconstruct(someId, dateTimeOccurred).unwrap();
153
153
+
}
154
154
+
```
155
155
+
156
156
+
### 5. Configure Queue Routing
157
157
+
158
158
+
**File:** `src/shared/infrastructure/events/BullMQEventPublisher.ts`
159
159
+
160
160
+
Update `getTargetQueues()` to route the event to appropriate queues:
161
161
+
162
162
+
```typescript
163
163
+
private getTargetQueues(eventName: EventName): QueueName[] {
164
164
+
switch (eventName) {
165
165
+
// ... existing cases
166
166
+
case EventNames.YOUR_NEW_EVENT:
167
167
+
return [QueueNames.NOTIFICATIONS]; // or multiple queues
168
168
+
default:
169
169
+
return [QueueNames.FEEDS];
170
170
+
}
171
171
+
}
172
172
+
```
173
173
+
174
174
+
### 6. Publish Events from the Use Case
175
175
+
176
176
+
**File:** The use case that performs the action (e.g., `src/modules/<module>/application/useCases/commands/YourUseCase.ts`)
177
177
+
178
178
+
Extend `BaseUseCase` and call `publishEventsForAggregate()`:
179
179
+
180
180
+
```typescript
181
181
+
import { BaseUseCase } from '../../../../../shared/core/UseCase';
182
182
+
import { IEventPublisher } from '../../../../../shared/application/events/IEventPublisher';
183
183
+
184
184
+
export class YourUseCase extends BaseUseCase<RequestDTO, Result<ResponseDTO>> {
185
185
+
constructor(
186
186
+
private repository: IRepository,
187
187
+
eventPublisher: IEventPublisher,
188
188
+
) {
189
189
+
super(eventPublisher);
190
190
+
}
191
191
+
192
192
+
async execute(request: RequestDTO): Promise<Result<ResponseDTO>> {
193
193
+
// ... business logic that raises domain events ...
194
194
+
195
195
+
// Publish events for the aggregate
196
196
+
const publishResult = await this.publishEventsForAggregate(entity);
197
197
+
if (publishResult.isErr()) {
198
198
+
console.error('Failed to publish events:', publishResult.error);
199
199
+
// Don't fail the operation if event publishing fails
200
200
+
}
201
201
+
202
202
+
return ok(response);
203
203
+
}
204
204
+
}
205
205
+
```
206
206
+
207
207
+
### 7. Update UseCaseFactory
208
208
+
209
209
+
**File:** `src/shared/infrastructure/http/factories/UseCaseFactory.ts`
210
210
+
211
211
+
Pass `eventPublisher` to the use case constructor:
212
212
+
213
213
+
```typescript
214
214
+
yourUseCase: new YourUseCase(
215
215
+
repositories.someRepository,
216
216
+
services.eventPublisher,
217
217
+
),
218
218
+
```
219
219
+
220
220
+
### 8. Create the Event Handler
221
221
+
222
222
+
**Location:** `src/modules/<module>/application/eventHandlers/YourNewEventHandler.ts`
223
223
+
224
224
+
```typescript
225
225
+
import { YourNewEvent } from '../../../<module>/domain/events/YourNewEvent';
226
226
+
import { IEventHandler } from '../../../../shared/application/events/IEventSubscriber';
227
227
+
import { Result, ok } from '../../../../shared/core/Result';
228
228
+
229
229
+
export class YourNewEventHandler implements IEventHandler<YourNewEvent> {
230
230
+
constructor(private someDependency: ISomeDependency) {}
231
231
+
232
232
+
async handle(event: YourNewEvent): Promise<Result<void>> {
233
233
+
// Handle the event
234
234
+
return ok(undefined);
235
235
+
}
236
236
+
}
237
237
+
```
238
238
+
239
239
+
### 9. Register the Handler in the Worker Process
240
240
+
241
241
+
**File:** `src/shared/infrastructure/processes/NotificationWorkerProcess.ts` (or appropriate worker)
242
242
+
243
243
+
```typescript
244
244
+
import { YourNewEventHandler } from '../../../modules/<module>/application/eventHandlers/YourNewEventHandler';
245
245
+
246
246
+
// In the initialization:
247
247
+
const yourNewEventHandler = new YourNewEventHandler(dependencies);
248
248
+
249
249
+
await subscriber.subscribe(EventNames.YOUR_NEW_EVENT, yourNewEventHandler);
250
250
+
```
251
251
+
252
252
+
---
253
253
+
254
254
+
## Event Aggregation with Sagas
255
255
+
256
256
+
For events that need to be bundled or coordinated, use a saga pattern:
257
257
+
258
258
+
**Example:** `src/modules/notifications/application/sagas/CardNotificationSaga.ts`
259
259
+
260
260
+
Sagas can:
261
261
+
262
262
+
- Aggregate multiple events within a time window
263
263
+
- Store pending state in Redis via `ISagaStateStore`
264
264
+
- Create consolidated notifications
265
265
+
266
266
+
Handler delegates to saga:
267
267
+
268
268
+
```typescript
269
269
+
export class YourEventHandler implements IEventHandler<YourEvent> {
270
270
+
constructor(private saga: YourSaga) {}
271
271
+
272
272
+
async handle(event: YourEvent): Promise<Result<void>> {
273
273
+
return this.saga.handleEvent(event);
274
274
+
}
275
275
+
}
276
276
+
```
277
277
+
278
278
+
---
279
279
+
280
280
+
## Summary Diagram
281
281
+
282
282
+
```
283
283
+
┌─────────────────────────────────────────────────────────────────────┐
284
284
+
│ PUBLISHING SIDE │
285
285
+
├─────────────────────────────────────────────────────────────────────┤
286
286
+
│ Domain Entity │
287
287
+
│ └── Raises event via this.addDomainEvent() │
288
288
+
│ ↓ │
289
289
+
│ Use Case (extends BaseUseCase) │
290
290
+
│ └── Calls publishEventsForAggregate() │
291
291
+
│ ↓ │
292
292
+
│ BullMQEventPublisher │
293
293
+
│ ├── EventMapper.toSerialized() - converts to JSON │
294
294
+
│ └── getTargetQueues() - routes to appropriate queue(s) │
295
295
+
│ ↓ │
296
296
+
│ Redis Queues (FEEDS, SEARCH, NOTIFICATIONS) │
297
297
+
└─────────────────────────────────────────────────────────────────────┘
298
298
+
↓
299
299
+
┌─────────────────────────────────────────────────────────────────────┐
300
300
+
│ CONSUMING SIDE │
301
301
+
├─────────────────────────────────────────────────────────────────────┤
302
302
+
│ Worker Process (e.g., NotificationWorkerProcess) │
303
303
+
│ └── Creates BullMQEventSubscriber │
304
304
+
│ ↓ │
305
305
+
│ BullMQEventSubscriber │
306
306
+
│ ├── Listens to queue │
307
307
+
│ ├── EventMapper.fromSerialized() - reconstructs event │
308
308
+
│ └── Dispatches to registered handler │
309
309
+
│ ↓ │
310
310
+
│ Event Handler / Saga │
311
311
+
│ └── Processes event (creates notifications, updates state, etc.) │
312
312
+
└─────────────────────────────────────────────────────────────────────┘
313
313
+
```
314
314
+
315
315
+
---
316
316
+
317
317
+
## Quick Reference: Files to Touch for a New Event
318
318
+
319
319
+
1. `src/shared/infrastructure/events/EventConfig.ts` — Add event name
320
320
+
2. `src/modules/<module>/domain/events/YourNewEvent.ts` — Create event class
321
321
+
3. `src/modules/<module>/domain/<Entity>.ts` — Raise event in domain method
322
322
+
4. `src/shared/infrastructure/events/EventMapper.ts` — Add serialization/deserialization
323
323
+
5. `src/shared/infrastructure/events/BullMQEventPublisher.ts` — Configure queue routing
324
324
+
6. `src/modules/<module>/application/useCases/...UseCase.ts` — Extend BaseUseCase, publish events
325
325
+
7. `src/shared/infrastructure/http/factories/UseCaseFactory.ts` — Pass eventPublisher
326
326
+
8. `src/modules/<module>/application/eventHandlers/...Handler.ts` — Create handler
327
327
+
9. `src/shared/infrastructure/processes/<Worker>Process.ts` — Register handler