···
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
···
1
1
+
```ts
2
2
+
agent.com.atproto.repo.listRecords
3
3
+
4
4
+
listRecords(
5
5
+
params?: ComAtprotoRepoListRecords.QueryParams,
6
6
+
opts?: ComAtprotoRepoListRecords.CallOptions,
7
7
+
): Promise<ComAtprotoRepoListRecords.Response> {
8
8
+
return this._client.call(
9
9
+
'com.atproto.repo.listRecords',
10
10
+
params,
11
11
+
undefined,
12
12
+
opts,
13
13
+
)
14
14
+
}
15
15
+
16
16
+
export type QueryParams = {
17
17
+
/** The handle or DID of the repo. */
18
18
+
repo: string
19
19
+
/** The NSID of the record type. */
20
20
+
collection: string
21
21
+
/** The number of records to return. */
22
22
+
limit?: number
23
23
+
cursor?: string
24
24
+
/** Flag to reverse the order of the returned records. */
25
25
+
reverse?: boolean
26
26
+
}
27
27
+
28
28
+
export interface Response {
29
29
+
success: boolean
30
30
+
headers: HeadersMap
31
31
+
data: OutputSchema
32
32
+
}
33
33
+
34
34
+
export interface OutputSchema {
35
35
+
cursor?: string
36
36
+
records: Record[]
37
37
+
}
38
38
+
39
39
+
export interface Record {
40
40
+
$type?: 'com.atproto.repo.listRecords#record'
41
41
+
uri: string
42
42
+
cid: string
43
43
+
value: { [_ in string]: unknown }
44
44
+
}
45
45
+
46
46
+
```