@vytches/ddd-aggregates 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -1332
- package/dist/index.cjs +1 -1
- package/dist/index.js +140 -535
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,1393 +1,206 @@
|
|
|
1
1
|
# @vytches/ddd-aggregates
|
|
2
2
|
|
|
3
|
+
> Aggregate root implementation with event sourcing, optimistic concurrency, and
|
|
4
|
+
> opt-in capabilities.
|
|
5
|
+
|
|
3
6
|
[](https://badge.fury.io/js/%40vytches%2Fddd-aggregates)
|
|
4
7
|
[](https://www.typescriptlang.org/)
|
|
5
8
|
[](https://opensource.org/licenses/MIT)
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Complete aggregate root implementation with built-in event sourcing, capability
|
|
10
|
-
system, and automatic event management. Designed for complex domain models with
|
|
11
|
-
strong consistency boundaries.
|
|
12
|
-
|
|
13
|
-
## 📚 Detailed Documentation
|
|
14
|
-
|
|
15
|
-
**NEW**: Enhanced Metadata System V2 with YAML Front Matter for AI-optimized
|
|
16
|
-
documentation:
|
|
17
|
-
|
|
18
|
-
- 🎯 **Generated JSDoc**: Full API documentation with hierarchical metadata
|
|
19
|
-
system
|
|
20
|
-
- 📊 **Live Documentation**: View at `/docs/api/aggregates/index.html` after
|
|
21
|
-
running `pnpm jsdoc:publish`
|
|
22
|
-
- 🧠 **AI-Optimized**: Structured metadata for LLM consumption and code
|
|
23
|
-
generation
|
|
24
|
-
- ⚡ **Performance**: ~12s initial build, ~2s incremental, ~0.5s no changes
|
|
25
|
-
|
|
26
|
-
**Core Documentation:**
|
|
27
|
-
|
|
28
|
-
- 📖 **AggregateRoot Class** - Base aggregate implementation with event sourcing
|
|
29
|
-
- 🔧 **AggregateBuilder Class** - Fluent builder for aggregates with
|
|
30
|
-
capabilities
|
|
31
|
-
- 🛠️ **Utility Functions** - Type-safe capability management and utilities
|
|
32
|
-
- 📋 **Error Classes** - Comprehensive aggregate-specific error handling
|
|
33
|
-
|
|
34
|
-
> **Enhanced Metadata System V2** provides hierarchical documentation (Global →
|
|
35
|
-
> Package → Class → Method) with format-specific content, custom tags, and
|
|
36
|
-
> automatic example injection.
|
|
37
|
-
|
|
38
|
-
## 📋 Table of Contents
|
|
39
|
-
|
|
40
|
-
- [Installation](#installation)
|
|
41
|
-
- [Key Features](#key-features)
|
|
42
|
-
- [Core Concepts](#core-concepts)
|
|
43
|
-
- [Quick Start](#quick-start)
|
|
44
|
-
- [Advanced Features](#advanced-features)
|
|
45
|
-
- [API Reference](#api-reference)
|
|
46
|
-
- [Patterns & Best Practices](#patterns--best-practices)
|
|
47
|
-
- [Event Sourcing](#event-sourcing)
|
|
48
|
-
- [Capabilities](#capabilities)
|
|
49
|
-
- [Testing](#testing)
|
|
50
|
-
- [Performance](#performance)
|
|
51
|
-
- [Contributing](#contributing)
|
|
52
|
-
|
|
53
|
-
## 🚀 Installation
|
|
10
|
+
## Installation
|
|
54
11
|
|
|
55
12
|
```bash
|
|
56
|
-
# npm
|
|
57
|
-
npm install @vytches/ddd-aggregates
|
|
58
|
-
|
|
59
|
-
# yarn
|
|
60
|
-
yarn add @vytches/ddd-aggregates
|
|
61
|
-
|
|
62
|
-
# pnpm
|
|
63
13
|
pnpm add @vytches/ddd-aggregates
|
|
64
14
|
```
|
|
65
15
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
# Required for full functionality
|
|
70
|
-
npm install @vytches/ddd-domain-primitives @vytches/ddd-events @vytches/ddd-utils
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## ✨ Key Features
|
|
74
|
-
|
|
75
|
-
### Aggregate Root Foundation
|
|
76
|
-
|
|
77
|
-
- **Event Sourcing**: Built-in event sourcing with automatic event management
|
|
78
|
-
- **Capability System**: Extensible capabilities for cross-cutting concerns
|
|
79
|
-
- **Concurrency Control**: Optimistic concurrency with version management
|
|
80
|
-
- **Snapshot Support**: Automatic snapshots for performance optimization
|
|
81
|
-
|
|
82
|
-
### Enterprise Features
|
|
83
|
-
|
|
84
|
-
- **Strong Consistency**: Aggregate consistency boundary enforcement
|
|
85
|
-
- **Event Management**: Automatic event collection and publishing
|
|
86
|
-
- **State Validation**: Built-in invariant validation
|
|
87
|
-
- **Audit Trail**: Complete audit trail with event history
|
|
88
|
-
|
|
89
|
-
### Developer Experience
|
|
90
|
-
|
|
91
|
-
- **Type Safety**: Full TypeScript support with generic typing
|
|
92
|
-
- **Fluent API**: Intuitive API for domain modeling
|
|
93
|
-
- **Testing Support**: Built-in testing utilities and builders
|
|
94
|
-
- **Documentation**: Comprehensive documentation and examples
|
|
16
|
+
## What's included
|
|
95
17
|
|
|
96
|
-
|
|
18
|
+
- **`AggregateRoot`** — base class for aggregate roots with identity,
|
|
19
|
+
versioning, and domain events
|
|
20
|
+
- **`Entity`** — base class for non-root domain entities (identity-based
|
|
21
|
+
equality, no event machinery)
|
|
22
|
+
- **`AggregateBuilder` / `aggregateBuilder`** — fluent builder for constructing
|
|
23
|
+
aggregates with capabilities
|
|
24
|
+
- **Capabilities** — `AuditCapability`, `EventSourcingCapability`,
|
|
25
|
+
`SnapshotCapability`, `VersioningCapability`
|
|
26
|
+
- **Utility functions** — type-safe capability casting and introspection
|
|
27
|
+
- **`AggregateError`** — error class for aggregate-specific failures
|
|
97
28
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
An aggregate root is the only member of an aggregate that outside objects are
|
|
101
|
-
allowed to hold references to. It's the gatekeeper for the aggregate, ensuring
|
|
102
|
-
consistency and managing the lifecycle of the aggregate.
|
|
103
|
-
|
|
104
|
-
```typescript
|
|
105
|
-
// Base aggregate root interface
|
|
106
|
-
interface IAggregateRoot {
|
|
107
|
-
readonly id: EntityId;
|
|
108
|
-
readonly version: number;
|
|
109
|
-
|
|
110
|
-
// Event management
|
|
111
|
-
addDomainEvent(event: IDomainEvent): void;
|
|
112
|
-
getUncommittedEvents(): IDomainEvent[];
|
|
113
|
-
markEventsAsCommitted(): void;
|
|
114
|
-
|
|
115
|
-
// Capability system
|
|
116
|
-
addCapability<T extends IAggregateCapability>(capability: T): void;
|
|
117
|
-
getCapability<T extends IAggregateCapability>(type: new () => T): T | null;
|
|
118
|
-
hasCapability<T extends IAggregateCapability>(type: new () => T): boolean;
|
|
119
|
-
|
|
120
|
-
// Lifecycle
|
|
121
|
-
toSnapshot(): AggregateSnapshot;
|
|
122
|
-
fromSnapshot(snapshot: AggregateSnapshot): void;
|
|
123
|
-
}
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Event Sourcing
|
|
127
|
-
|
|
128
|
-
Event sourcing is a pattern where state changes are stored as a sequence of
|
|
129
|
-
events. The current state is derived by replaying these events.
|
|
130
|
-
|
|
131
|
-
```typescript
|
|
132
|
-
// Event sourced aggregate
|
|
133
|
-
abstract class EventSourcedAggregate extends AggregateRoot {
|
|
134
|
-
// Apply event to current state
|
|
135
|
-
protected apply(event: IDomainEvent): void;
|
|
136
|
-
|
|
137
|
-
// Replay events to rebuild state
|
|
138
|
-
static fromEventStream<T extends EventSourcedAggregate>(
|
|
139
|
-
events: IDomainEvent[]
|
|
140
|
-
): T;
|
|
141
|
-
|
|
142
|
-
// Event handlers (implemented by subclasses)
|
|
143
|
-
protected abstract getEventHandlers(): EventHandlerMap;
|
|
144
|
-
}
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### Capabilities
|
|
148
|
-
|
|
149
|
-
Capabilities are modular extensions that can be added to aggregates to provide
|
|
150
|
-
cross-cutting concerns like auditing, caching, or validation.
|
|
29
|
+
## Usage
|
|
151
30
|
|
|
152
31
|
```typescript
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// Lifecycle hooks
|
|
158
|
-
onEventApplied?(event: IDomainEvent): void;
|
|
159
|
-
onSnapshotCreated?(snapshot: AggregateSnapshot): void;
|
|
160
|
-
beforeSave?(): void;
|
|
161
|
-
afterSave?(): void;
|
|
162
|
-
}
|
|
163
|
-
```
|
|
32
|
+
import { AggregateRoot } from '@vytches/ddd-aggregates';
|
|
33
|
+
import type { IAggregateConstructorParams } from '@vytches/ddd-aggregates';
|
|
34
|
+
import { EntityId } from '@vytches/ddd-contracts';
|
|
164
35
|
|
|
165
|
-
|
|
36
|
+
class Order extends AggregateRoot<string> {
|
|
37
|
+
private customerId = '';
|
|
38
|
+
private status: 'pending' | 'confirmed' = 'pending';
|
|
166
39
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
// Domain events
|
|
175
|
-
class OrderCreatedEvent extends DomainEvent<{
|
|
176
|
-
orderId: string;
|
|
177
|
-
customerId: string;
|
|
178
|
-
totalAmount: number;
|
|
179
|
-
currency: string;
|
|
180
|
-
}> {
|
|
181
|
-
constructor(payload: OrderCreatedData) {
|
|
182
|
-
super(payload, undefined, 'OrderCreated');
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
class OrderItemAddedEvent extends DomainEvent<{
|
|
187
|
-
orderId: string;
|
|
188
|
-
productId: string;
|
|
189
|
-
quantity: number;
|
|
190
|
-
price: number;
|
|
191
|
-
}> {
|
|
192
|
-
constructor(payload: OrderItemAddedData) {
|
|
193
|
-
super(payload, undefined, 'OrderItemAdded');
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Value objects
|
|
198
|
-
class OrderStatus {
|
|
199
|
-
static readonly PENDING = new OrderStatus('PENDING');
|
|
200
|
-
static readonly CONFIRMED = new OrderStatus('CONFIRMED');
|
|
201
|
-
static readonly SHIPPED = new OrderStatus('SHIPPED');
|
|
202
|
-
static readonly DELIVERED = new OrderStatus('DELIVERED');
|
|
203
|
-
static readonly CANCELLED = new OrderStatus('CANCELLED');
|
|
204
|
-
|
|
205
|
-
private constructor(private readonly value: string) {}
|
|
206
|
-
|
|
207
|
-
equals(other: OrderStatus): boolean {
|
|
208
|
-
return this.value === other.value;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
toString(): string {
|
|
212
|
-
return this.value;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Aggregate root implementation
|
|
217
|
-
class OrderAggregate extends AggregateRoot {
|
|
218
|
-
private customerId: EntityId;
|
|
219
|
-
private items: OrderItem[] = [];
|
|
220
|
-
private status: OrderStatus = OrderStatus.PENDING;
|
|
221
|
-
private totalAmount: number = 0;
|
|
222
|
-
private currency: string = 'USD';
|
|
223
|
-
|
|
224
|
-
private constructor(id: EntityId, customerId: EntityId) {
|
|
225
|
-
super(id);
|
|
226
|
-
this.customerId = customerId;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Factory method
|
|
230
|
-
static create(customerId: EntityId): OrderAggregate {
|
|
231
|
-
const order = new OrderAggregate(EntityId.create(), customerId);
|
|
232
|
-
|
|
233
|
-
// Add creation event
|
|
234
|
-
order.addDomainEvent(
|
|
235
|
-
new OrderCreatedEvent({
|
|
236
|
-
orderId: order.id.value,
|
|
237
|
-
customerId: customerId.value,
|
|
238
|
-
totalAmount: 0,
|
|
239
|
-
currency: 'USD',
|
|
240
|
-
})
|
|
40
|
+
constructor(params: IAggregateConstructorParams<string>) {
|
|
41
|
+
super(params);
|
|
42
|
+
this.registerEventHandler<{ customerId: string }>(
|
|
43
|
+
'OrderCreated',
|
|
44
|
+
payload => {
|
|
45
|
+
this.customerId = payload!.customerId;
|
|
46
|
+
}
|
|
241
47
|
);
|
|
242
|
-
|
|
243
|
-
|
|
48
|
+
this.registerEventHandler<void>('OrderConfirmed', () => {
|
|
49
|
+
this.status = 'confirmed';
|
|
50
|
+
});
|
|
244
51
|
}
|
|
245
52
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
price: number
|
|
251
|
-
): Result<void, Error> {
|
|
252
|
-
// Validate business rules
|
|
253
|
-
if (quantity <= 0) {
|
|
254
|
-
return Result.fail(new Error('Quantity must be positive'));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (price < 0) {
|
|
258
|
-
return Result.fail(new Error('Price cannot be negative'));
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (!this.status.equals(OrderStatus.PENDING)) {
|
|
262
|
-
return Result.fail(new Error('Cannot add items to non-pending order'));
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Apply changes
|
|
266
|
-
const item = new OrderItem(productId, quantity, price);
|
|
267
|
-
this.items.push(item);
|
|
268
|
-
this.totalAmount += quantity * price;
|
|
269
|
-
|
|
270
|
-
// Add domain event
|
|
271
|
-
this.addDomainEvent(
|
|
272
|
-
new OrderItemAddedEvent({
|
|
273
|
-
orderId: this.id.value,
|
|
274
|
-
productId: productId.value,
|
|
275
|
-
quantity,
|
|
276
|
-
price,
|
|
277
|
-
})
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
return Result.ok();
|
|
53
|
+
static create(customerId: string): Order {
|
|
54
|
+
const order = new Order({ id: EntityId.create(), version: 0 });
|
|
55
|
+
order.apply('OrderCreated', { customerId });
|
|
56
|
+
return order;
|
|
281
57
|
}
|
|
282
58
|
|
|
283
|
-
confirm():
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (this.items.length === 0) {
|
|
289
|
-
return Result.fail(new Error('Cannot confirm empty order'));
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
this.status = OrderStatus.CONFIRMED;
|
|
293
|
-
|
|
294
|
-
this.addDomainEvent(
|
|
295
|
-
new OrderConfirmedEvent({
|
|
296
|
-
orderId: this.id.value,
|
|
297
|
-
confirmedAt: new Date(),
|
|
298
|
-
totalAmount: this.totalAmount,
|
|
299
|
-
})
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
return Result.ok();
|
|
59
|
+
confirm(): void {
|
|
60
|
+
if (this.status !== 'pending') throw new Error('Order is not pending');
|
|
61
|
+
this.apply('OrderConfirmed', undefined);
|
|
303
62
|
}
|
|
304
63
|
|
|
305
|
-
|
|
306
|
-
getCustomerId(): EntityId {
|
|
64
|
+
getCustomerId(): string {
|
|
307
65
|
return this.customerId;
|
|
308
66
|
}
|
|
309
|
-
|
|
310
|
-
getItems(): ReadonlyArray<OrderItem> {
|
|
311
|
-
return this.items;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
getStatus(): OrderStatus {
|
|
67
|
+
getStatus(): string {
|
|
315
68
|
return this.status;
|
|
316
69
|
}
|
|
317
|
-
|
|
318
|
-
getTotalAmount(): number {
|
|
319
|
-
return this.totalAmount;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
getCurrency(): string {
|
|
323
|
-
return this.currency;
|
|
324
|
-
}
|
|
325
70
|
}
|
|
326
71
|
|
|
327
|
-
//
|
|
328
|
-
const order =
|
|
329
|
-
|
|
330
|
-
const addResult = order.addItem(EntityId.from('product-456'), 2, 29.99);
|
|
72
|
+
// Create an order
|
|
73
|
+
const order = Order.create('customer-123');
|
|
74
|
+
order.confirm();
|
|
331
75
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (confirmResult.isSuccess()) {
|
|
336
|
-
console.log('Order confirmed:', order.id.value);
|
|
337
|
-
console.log('Events:', order.getUncommittedEvents());
|
|
338
|
-
}
|
|
339
|
-
}
|
|
76
|
+
// Domain events are collected until committed
|
|
77
|
+
const events = order.getDomainEvents(); // [OrderCreated, OrderConfirmed]
|
|
78
|
+
order.commit(); // clears collected events
|
|
340
79
|
```
|
|
341
80
|
|
|
342
|
-
###
|
|
81
|
+
### With capabilities
|
|
343
82
|
|
|
344
83
|
```typescript
|
|
345
84
|
import {
|
|
346
|
-
|
|
347
|
-
|
|
85
|
+
aggregateBuilder,
|
|
86
|
+
AuditCapability,
|
|
87
|
+
SnapshotCapability,
|
|
348
88
|
} from '@vytches/ddd-aggregates';
|
|
349
89
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
private constructor() {
|
|
356
|
-
super();
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Factory method
|
|
360
|
-
static create(
|
|
361
|
-
accountId: EntityId,
|
|
362
|
-
initialDeposit: number,
|
|
363
|
-
overdraftLimit: number = 0
|
|
364
|
-
): BankAccountAggregate {
|
|
365
|
-
const account = new BankAccountAggregate();
|
|
366
|
-
|
|
367
|
-
// Apply creation event
|
|
368
|
-
account.apply(
|
|
369
|
-
new AccountOpenedEvent({
|
|
370
|
-
accountId: accountId.value,
|
|
371
|
-
initialDeposit,
|
|
372
|
-
overdraftLimit,
|
|
373
|
-
openedAt: new Date(),
|
|
374
|
-
})
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
return account;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Business methods
|
|
381
|
-
deposit(amount: number): Result<void, Error> {
|
|
382
|
-
if (amount <= 0) {
|
|
383
|
-
return Result.fail(new Error('Deposit amount must be positive'));
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (this.status !== AccountStatus.ACTIVE) {
|
|
387
|
-
return Result.fail(new Error('Account is not active'));
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
this.apply(
|
|
391
|
-
new MoneyDepositedEvent({
|
|
392
|
-
accountId: this.id.value,
|
|
393
|
-
amount,
|
|
394
|
-
balance: this.balance + amount,
|
|
395
|
-
depositedAt: new Date(),
|
|
396
|
-
})
|
|
397
|
-
);
|
|
398
|
-
|
|
399
|
-
return Result.ok();
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
withdraw(amount: number): Result<void, Error> {
|
|
403
|
-
if (amount <= 0) {
|
|
404
|
-
return Result.fail(new Error('Withdrawal amount must be positive'));
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (this.status !== AccountStatus.ACTIVE) {
|
|
408
|
-
return Result.fail(new Error('Account is not active'));
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const newBalance = this.balance - amount;
|
|
412
|
-
if (newBalance < -this.overdraftLimit) {
|
|
413
|
-
return Result.fail(new Error('Insufficient funds'));
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
this.apply(
|
|
417
|
-
new MoneyWithdrawnEvent({
|
|
418
|
-
accountId: this.id.value,
|
|
419
|
-
amount,
|
|
420
|
-
balance: newBalance,
|
|
421
|
-
withdrawnAt: new Date(),
|
|
422
|
-
})
|
|
423
|
-
);
|
|
424
|
-
|
|
425
|
-
return Result.ok();
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Event handlers
|
|
429
|
-
protected getEventHandlers(): EventHandlerMap {
|
|
430
|
-
return {
|
|
431
|
-
AccountOpened: this.onAccountOpened.bind(this),
|
|
432
|
-
MoneyDeposited: this.onMoneyDeposited.bind(this),
|
|
433
|
-
MoneyWithdrawn: this.onMoneyWithdrawn.bind(this),
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
private onAccountOpened(event: AccountOpenedEvent): void {
|
|
438
|
-
this.id = EntityId.from(event.payload.accountId);
|
|
439
|
-
this.balance = event.payload.initialDeposit;
|
|
440
|
-
this.overdraftLimit = event.payload.overdraftLimit;
|
|
441
|
-
this.status = AccountStatus.ACTIVE;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
private onMoneyDeposited(event: MoneyDepositedEvent): void {
|
|
445
|
-
this.balance = event.payload.balance;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
private onMoneyWithdrawn(event: MoneyWithdrawnEvent): void {
|
|
449
|
-
this.balance = event.payload.balance;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// Getters
|
|
453
|
-
getBalance(): number {
|
|
454
|
-
return this.balance;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
getStatus(): AccountStatus {
|
|
458
|
-
return this.status;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
getOverdraftLimit(): number {
|
|
462
|
-
return this.overdraftLimit;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Reconstitute from event stream
|
|
467
|
-
const events = await eventStore.getEventStream('account-123');
|
|
468
|
-
const account = BankAccountAggregate.fromEventStream(events);
|
|
469
|
-
|
|
470
|
-
console.log('Current balance:', account.getBalance());
|
|
90
|
+
const order = aggregateBuilder(Order)
|
|
91
|
+
.withCapability(AuditCapability)
|
|
92
|
+
.withCapability(SnapshotCapability)
|
|
93
|
+
.build({ id: EntityId.create(), version: 0 });
|
|
471
94
|
```
|
|
472
95
|
|
|
473
|
-
###
|
|
96
|
+
### Utility functions
|
|
474
97
|
|
|
475
98
|
```typescript
|
|
476
99
|
import {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
100
|
+
asSnapshotAggregate,
|
|
101
|
+
tryAsSnapshotAggregate,
|
|
102
|
+
hasAllCapabilities,
|
|
103
|
+
getAggregateCapabilities,
|
|
481
104
|
} from '@vytches/ddd-aggregates';
|
|
105
|
+
import { SnapshotCapability, AuditCapability } from '@vytches/ddd-aggregates';
|
|
482
106
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
private stock: number;
|
|
487
|
-
private category: string;
|
|
107
|
+
// Type-safe capability access
|
|
108
|
+
const snapshotable = asSnapshotAggregate(order); // throws if missing
|
|
109
|
+
const maybeSnapshot = tryAsSnapshotAggregate(order); // returns null if missing
|
|
488
110
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
price: number,
|
|
493
|
-
stock: number,
|
|
494
|
-
category: string
|
|
495
|
-
) {
|
|
496
|
-
super(id);
|
|
497
|
-
this.name = name;
|
|
498
|
-
this.price = price;
|
|
499
|
-
this.stock = stock;
|
|
500
|
-
this.category = category;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
static create(
|
|
504
|
-
name: string,
|
|
505
|
-
price: number,
|
|
506
|
-
initialStock: number,
|
|
507
|
-
category: string
|
|
508
|
-
): ProductAggregate {
|
|
509
|
-
const product = new ProductAggregate(
|
|
510
|
-
EntityId.create(),
|
|
511
|
-
name,
|
|
512
|
-
price,
|
|
513
|
-
initialStock,
|
|
514
|
-
category
|
|
515
|
-
);
|
|
516
|
-
|
|
517
|
-
// Add capabilities
|
|
518
|
-
product.addCapability(new AuditCapability());
|
|
519
|
-
product.addCapability(new CacheCapability({ ttl: 300 })); // 5 minutes
|
|
520
|
-
product.addCapability(new ValidationCapability());
|
|
521
|
-
|
|
522
|
-
product.addDomainEvent(
|
|
523
|
-
new ProductCreatedEvent({
|
|
524
|
-
productId: product.id.value,
|
|
525
|
-
name,
|
|
526
|
-
price,
|
|
527
|
-
stock: initialStock,
|
|
528
|
-
category,
|
|
529
|
-
})
|
|
530
|
-
);
|
|
531
|
-
|
|
532
|
-
return product;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
updatePrice(newPrice: number): Result<void, Error> {
|
|
536
|
-
// Validation capability automatically validates
|
|
537
|
-
if (newPrice < 0) {
|
|
538
|
-
return Result.fail(new Error('Price cannot be negative'));
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const oldPrice = this.price;
|
|
542
|
-
this.price = newPrice;
|
|
543
|
-
|
|
544
|
-
// Audit capability automatically logs changes
|
|
545
|
-
this.addDomainEvent(
|
|
546
|
-
new ProductPriceUpdatedEvent({
|
|
547
|
-
productId: this.id.value,
|
|
548
|
-
oldPrice,
|
|
549
|
-
newPrice,
|
|
550
|
-
updatedAt: new Date(),
|
|
551
|
-
})
|
|
552
|
-
);
|
|
553
|
-
|
|
554
|
-
return Result.ok();
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
adjustStock(quantity: number): Result<void, Error> {
|
|
558
|
-
const newStock = this.stock + quantity;
|
|
559
|
-
|
|
560
|
-
if (newStock < 0) {
|
|
561
|
-
return Result.fail(new Error('Insufficient stock'));
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
this.stock = newStock;
|
|
565
|
-
|
|
566
|
-
this.addDomainEvent(
|
|
567
|
-
new ProductStockAdjustedEvent({
|
|
568
|
-
productId: this.id.value,
|
|
569
|
-
adjustment: quantity,
|
|
570
|
-
newStock,
|
|
571
|
-
adjustedAt: new Date(),
|
|
572
|
-
})
|
|
573
|
-
);
|
|
574
|
-
|
|
575
|
-
return Result.ok();
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// Getters
|
|
579
|
-
getName(): string {
|
|
580
|
-
return this.name;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
getPrice(): number {
|
|
584
|
-
return this.price;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
getStock(): number {
|
|
588
|
-
return this.stock;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
getCategory(): string {
|
|
592
|
-
return this.category;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Usage with capabilities
|
|
597
|
-
const product = ProductAggregate.create(
|
|
598
|
-
'Gaming Laptop',
|
|
599
|
-
1299.99,
|
|
600
|
-
50,
|
|
601
|
-
'Electronics'
|
|
602
|
-
);
|
|
603
|
-
|
|
604
|
-
// Audit capability tracks all changes
|
|
605
|
-
product.updatePrice(1199.99);
|
|
606
|
-
product.adjustStock(-5);
|
|
607
|
-
|
|
608
|
-
// Cache capability optimizes repeated access
|
|
609
|
-
const auditCapability = product.getCapability(AuditCapability);
|
|
610
|
-
console.log('Audit trail:', auditCapability.getAuditTrail());
|
|
611
|
-
|
|
612
|
-
// Validation capability ensures data integrity
|
|
613
|
-
const validationCapability = product.getCapability(ValidationCapability);
|
|
614
|
-
console.log('Validation rules:', validationCapability.getValidationRules());
|
|
111
|
+
// Introspection
|
|
112
|
+
const hasAll = hasAllCapabilities(order, [SnapshotCapability, AuditCapability]);
|
|
113
|
+
const capabilities = getAggregateCapabilities(order);
|
|
615
114
|
```
|
|
616
115
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
### Optimistic Concurrency Control
|
|
116
|
+
### Non-root entities
|
|
620
117
|
|
|
621
118
|
```typescript
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
updateShippingAddress(newAddress: Address): Result<void, Error> {
|
|
626
|
-
// Check if aggregate is in correct state
|
|
627
|
-
if (this.status !== OrderStatus.CONFIRMED) {
|
|
628
|
-
return Result.fail(new Error('Cannot update shipping address'));
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// Apply change
|
|
632
|
-
this.shippingAddress = newAddress;
|
|
119
|
+
import { Entity } from '@vytches/ddd-aggregates';
|
|
120
|
+
import { EntityId } from '@vytches/ddd-contracts';
|
|
633
121
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
);
|
|
642
|
-
|
|
643
|
-
return Result.ok();
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// Repository handles concurrency conflicts
|
|
648
|
-
class OrderRepository {
|
|
649
|
-
async save(order: OrderAggregate): Promise<void> {
|
|
650
|
-
try {
|
|
651
|
-
await this.db.orders.update({
|
|
652
|
-
where: {
|
|
653
|
-
id: order.id.value,
|
|
654
|
-
version: order.version - 1, // Expected version
|
|
655
|
-
},
|
|
656
|
-
data: {
|
|
657
|
-
...order.toSnapshot(),
|
|
658
|
-
version: order.version,
|
|
659
|
-
},
|
|
660
|
-
});
|
|
661
|
-
} catch (error) {
|
|
662
|
-
if (error.code === 'P2025') {
|
|
663
|
-
// Prisma: Record not found
|
|
664
|
-
throw new OptimisticConcurrencyError(
|
|
665
|
-
`Order ${order.id.value} was modified by another process`
|
|
666
|
-
);
|
|
667
|
-
}
|
|
668
|
-
throw error;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
```
|
|
673
|
-
|
|
674
|
-
### Snapshot Support
|
|
675
|
-
|
|
676
|
-
```typescript
|
|
677
|
-
class OrderAggregate extends AggregateRoot {
|
|
678
|
-
// Snapshot creation
|
|
679
|
-
toSnapshot(): AggregateSnapshot {
|
|
680
|
-
return {
|
|
681
|
-
id: this.id.value,
|
|
682
|
-
version: this.version,
|
|
683
|
-
data: {
|
|
684
|
-
customerId: this.customerId.value,
|
|
685
|
-
items: this.items.map(item => item.toJSON()),
|
|
686
|
-
status: this.status.toString(),
|
|
687
|
-
totalAmount: this.totalAmount,
|
|
688
|
-
currency: this.currency,
|
|
689
|
-
createdAt: this.createdAt.toISOString(),
|
|
690
|
-
updatedAt: new Date().toISOString(),
|
|
691
|
-
},
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
// Snapshot restoration
|
|
696
|
-
fromSnapshot(snapshot: AggregateSnapshot): void {
|
|
697
|
-
this.id = EntityId.from(snapshot.id);
|
|
698
|
-
this.version = snapshot.version;
|
|
699
|
-
this.customerId = EntityId.from(snapshot.data.customerId);
|
|
700
|
-
this.items = snapshot.data.items.map(item => OrderItem.fromJSON(item));
|
|
701
|
-
this.status = OrderStatus.fromString(snapshot.data.status);
|
|
702
|
-
this.totalAmount = snapshot.data.totalAmount;
|
|
703
|
-
this.currency = snapshot.data.currency;
|
|
704
|
-
this.createdAt = new Date(snapshot.data.createdAt);
|
|
705
|
-
this.updatedAt = new Date(snapshot.data.updatedAt);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Automatic snapshot creation
|
|
710
|
-
class OrderRepository {
|
|
711
|
-
async save(order: OrderAggregate): Promise<void> {
|
|
712
|
-
// Save aggregate snapshot
|
|
713
|
-
await this.db.orders.upsert({
|
|
714
|
-
where: { id: order.id.value },
|
|
715
|
-
update: order.toSnapshot(),
|
|
716
|
-
create: order.toSnapshot(),
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
// Save events
|
|
720
|
-
const events = order.getUncommittedEvents();
|
|
721
|
-
await this.eventStore.append(order.id.value, events);
|
|
722
|
-
|
|
723
|
-
// Create snapshot if threshold reached
|
|
724
|
-
if (events.length >= this.snapshotThreshold) {
|
|
725
|
-
await this.snapshotStore.createSnapshot(
|
|
726
|
-
order.id.value,
|
|
727
|
-
order.toSnapshot()
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
```
|
|
733
|
-
|
|
734
|
-
### Invariant Validation
|
|
735
|
-
|
|
736
|
-
```typescript
|
|
737
|
-
class OrderAggregate extends AggregateRoot {
|
|
738
|
-
// Invariant validation
|
|
739
|
-
private validateInvariants(): void {
|
|
740
|
-
// Business rules that must always be true
|
|
741
|
-
if (this.totalAmount < 0) {
|
|
742
|
-
throw new InvariantViolationError('Total amount cannot be negative');
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
if (this.items.length === 0 && this.status !== OrderStatus.PENDING) {
|
|
746
|
-
throw new InvariantViolationError('Non-pending orders must have items');
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
if (this.status === OrderStatus.CONFIRMED && this.totalAmount === 0) {
|
|
750
|
-
throw new InvariantViolationError(
|
|
751
|
-
'Confirmed orders must have positive total'
|
|
752
|
-
);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// Validate item consistency
|
|
756
|
-
const calculatedTotal = this.items.reduce(
|
|
757
|
-
(sum, item) => sum + item.quantity * item.price,
|
|
758
|
-
0
|
|
759
|
-
);
|
|
760
|
-
|
|
761
|
-
if (Math.abs(calculatedTotal - this.totalAmount) > 0.01) {
|
|
762
|
-
throw new InvariantViolationError('Total amount does not match items');
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// Called automatically before events are applied
|
|
767
|
-
protected beforeApplyEvent(event: IDomainEvent): void {
|
|
768
|
-
this.validateInvariants();
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
// Called automatically after events are applied
|
|
772
|
-
protected afterApplyEvent(event: IDomainEvent): void {
|
|
773
|
-
this.validateInvariants();
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
```
|
|
777
|
-
|
|
778
|
-
## 📚 API Reference
|
|
779
|
-
|
|
780
|
-
### AggregateRoot
|
|
781
|
-
|
|
782
|
-
**Base Class:**
|
|
783
|
-
|
|
784
|
-
```typescript
|
|
785
|
-
abstract class AggregateRoot {
|
|
786
|
-
protected readonly id: EntityId;
|
|
787
|
-
protected version: number;
|
|
788
|
-
|
|
789
|
-
// Event management
|
|
790
|
-
addDomainEvent(event: IDomainEvent): void;
|
|
791
|
-
getUncommittedEvents(): IDomainEvent[];
|
|
792
|
-
markEventsAsCommitted(): void;
|
|
793
|
-
clearEvents(): void;
|
|
794
|
-
|
|
795
|
-
// Capability system
|
|
796
|
-
addCapability<T extends IAggregateCapability>(capability: T): void;
|
|
797
|
-
getCapability<T extends IAggregateCapability>(type: new () => T): T | null;
|
|
798
|
-
hasCapability<T extends IAggregateCapability>(type: new () => T): boolean;
|
|
799
|
-
removeCapability<T extends IAggregateCapability>(type: new () => T): void;
|
|
800
|
-
|
|
801
|
-
// Snapshot support
|
|
802
|
-
toSnapshot(): AggregateSnapshot;
|
|
803
|
-
fromSnapshot(snapshot: AggregateSnapshot): void;
|
|
804
|
-
|
|
805
|
-
// Lifecycle hooks
|
|
806
|
-
protected beforeApplyEvent?(event: IDomainEvent): void;
|
|
807
|
-
protected afterApplyEvent?(event: IDomainEvent): void;
|
|
808
|
-
protected beforeSave?(): void;
|
|
809
|
-
protected afterSave?(): void;
|
|
810
|
-
}
|
|
811
|
-
```
|
|
812
|
-
|
|
813
|
-
### EventSourcedAggregate
|
|
814
|
-
|
|
815
|
-
**Event Sourcing Support:**
|
|
816
|
-
|
|
817
|
-
```typescript
|
|
818
|
-
abstract class EventSourcedAggregate extends AggregateRoot {
|
|
819
|
-
// Event application
|
|
820
|
-
protected apply(event: IDomainEvent): void;
|
|
821
|
-
|
|
822
|
-
// Event stream reconstruction
|
|
823
|
-
static fromEventStream<T extends EventSourcedAggregate>(
|
|
824
|
-
events: IDomainEvent[]
|
|
825
|
-
): T;
|
|
826
|
-
|
|
827
|
-
// Event handlers (must be implemented)
|
|
828
|
-
protected abstract getEventHandlers(): EventHandlerMap;
|
|
829
|
-
|
|
830
|
-
// Replay events
|
|
831
|
-
protected replayEvents(events: IDomainEvent[]): void;
|
|
832
|
-
|
|
833
|
-
// Get aggregate version from events
|
|
834
|
-
protected getVersionFromEvents(events: IDomainEvent[]): number;
|
|
835
|
-
}
|
|
836
|
-
```
|
|
837
|
-
|
|
838
|
-
### Built-in Capabilities
|
|
839
|
-
|
|
840
|
-
**Audit Capability:**
|
|
841
|
-
|
|
842
|
-
```typescript
|
|
843
|
-
class AuditCapability implements IAggregateCapability {
|
|
844
|
-
readonly name = 'audit';
|
|
845
|
-
|
|
846
|
-
// Configuration
|
|
847
|
-
constructor(config?: AuditConfig) {}
|
|
848
|
-
|
|
849
|
-
// Audit trail
|
|
850
|
-
getAuditTrail(): AuditEntry[];
|
|
851
|
-
clearAuditTrail(): void;
|
|
852
|
-
|
|
853
|
-
// Hooks
|
|
854
|
-
onEventApplied(event: IDomainEvent): void;
|
|
855
|
-
onSnapshotCreated(snapshot: AggregateSnapshot): void;
|
|
856
|
-
}
|
|
857
|
-
```
|
|
858
|
-
|
|
859
|
-
**Cache Capability:**
|
|
860
|
-
|
|
861
|
-
```typescript
|
|
862
|
-
class CacheCapability implements IAggregateCapability {
|
|
863
|
-
readonly name = 'cache';
|
|
864
|
-
|
|
865
|
-
// Configuration
|
|
866
|
-
constructor(config: CacheConfig) {}
|
|
867
|
-
|
|
868
|
-
// Cache management
|
|
869
|
-
get<T>(key: string): T | undefined;
|
|
870
|
-
set<T>(key: string, value: T): void;
|
|
871
|
-
remove(key: string): void;
|
|
872
|
-
clear(): void;
|
|
873
|
-
|
|
874
|
-
// Statistics
|
|
875
|
-
getStats(): CacheStats;
|
|
876
|
-
}
|
|
877
|
-
```
|
|
878
|
-
|
|
879
|
-
**Validation Capability:**
|
|
880
|
-
|
|
881
|
-
```typescript
|
|
882
|
-
class ValidationCapability implements IAggregateCapability {
|
|
883
|
-
readonly name = 'validation';
|
|
884
|
-
|
|
885
|
-
// Configuration
|
|
886
|
-
constructor(config?: ValidationConfig) {}
|
|
887
|
-
|
|
888
|
-
// Validation rules
|
|
889
|
-
addRule(rule: ValidationRule): void;
|
|
890
|
-
removeRule(ruleName: string): void;
|
|
891
|
-
getValidationRules(): ValidationRule[];
|
|
892
|
-
|
|
893
|
-
// Validation
|
|
894
|
-
validate(aggregate: AggregateRoot): ValidationResult;
|
|
895
|
-
|
|
896
|
-
// Hooks
|
|
897
|
-
beforeApplyEvent(event: IDomainEvent): void;
|
|
898
|
-
}
|
|
899
|
-
```
|
|
900
|
-
|
|
901
|
-
## 🎯 Patterns & Best Practices
|
|
902
|
-
|
|
903
|
-
### Aggregate Design Principles
|
|
904
|
-
|
|
905
|
-
1. **Keep Aggregates Small**
|
|
906
|
-
|
|
907
|
-
```typescript
|
|
908
|
-
// Good: Small, focused aggregate
|
|
909
|
-
class OrderAggregate extends AggregateRoot {
|
|
910
|
-
private items: OrderItem[] = [];
|
|
911
|
-
private status: OrderStatus;
|
|
912
|
-
|
|
913
|
-
// Only essential order operations
|
|
914
|
-
addItem(item: OrderItem): Result<void, Error> {
|
|
915
|
-
/* ... */
|
|
916
|
-
}
|
|
917
|
-
removeItem(itemId: string): Result<void, Error> {
|
|
918
|
-
/* ... */
|
|
919
|
-
}
|
|
920
|
-
confirm(): Result<void, Error> {
|
|
921
|
-
/* ... */
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// Avoid: Large, complex aggregates
|
|
926
|
-
class OrderAggregate extends AggregateRoot {
|
|
927
|
-
private items: OrderItem[] = [];
|
|
928
|
-
private customer: Customer; // Should be separate aggregate
|
|
929
|
-
private inventory: Inventory; // Should be separate aggregate
|
|
930
|
-
private payments: Payment[]; // Should be separate aggregate
|
|
931
|
-
}
|
|
932
|
-
```
|
|
933
|
-
|
|
934
|
-
2. **Use Eventual Consistency Between Aggregates**
|
|
935
|
-
|
|
936
|
-
```typescript
|
|
937
|
-
// Good: Eventual consistency via domain events
|
|
938
|
-
class OrderAggregate extends AggregateRoot {
|
|
939
|
-
confirm(): Result<void, Error> {
|
|
940
|
-
this.status = OrderStatus.CONFIRMED;
|
|
941
|
-
|
|
942
|
-
// Notify other aggregates via events
|
|
943
|
-
this.addDomainEvent(
|
|
944
|
-
new OrderConfirmedEvent({
|
|
945
|
-
orderId: this.id.value,
|
|
946
|
-
customerId: this.customerId.value,
|
|
947
|
-
items: this.items.map(item => item.toJSON()),
|
|
948
|
-
})
|
|
949
|
-
);
|
|
950
|
-
|
|
951
|
-
return Result.ok();
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
// Handler in inventory context
|
|
956
|
-
@EventHandler(OrderConfirmedEvent)
|
|
957
|
-
class InventoryHandler {
|
|
958
|
-
async handle(event: OrderConfirmedEvent): Promise<void> {
|
|
959
|
-
await this.inventoryService.reserveItems(event.payload.items);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
```
|
|
963
|
-
|
|
964
|
-
3. **Enforce Invariants at Aggregate Boundaries**
|
|
965
|
-
|
|
966
|
-
```typescript
|
|
967
|
-
class OrderAggregate extends AggregateRoot {
|
|
968
|
-
private static readonly MAX_ITEMS = 100;
|
|
969
|
-
private static readonly MIN_TOTAL = 10.0;
|
|
970
|
-
|
|
971
|
-
addItem(item: OrderItem): Result<void, Error> {
|
|
972
|
-
// Validate aggregate invariants
|
|
973
|
-
if (this.items.length >= OrderAggregate.MAX_ITEMS) {
|
|
974
|
-
return Result.fail(
|
|
975
|
-
new Error(
|
|
976
|
-
`Order cannot have more than ${OrderAggregate.MAX_ITEMS} items`
|
|
977
|
-
)
|
|
978
|
-
);
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
const newTotal = this.calculateTotalWithItem(item);
|
|
982
|
-
if (
|
|
983
|
-
this.status === OrderStatus.PENDING &&
|
|
984
|
-
newTotal < OrderAggregate.MIN_TOTAL
|
|
985
|
-
) {
|
|
986
|
-
return Result.fail(
|
|
987
|
-
new Error(`Order total must be at least ${OrderAggregate.MIN_TOTAL}`)
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
// Apply change
|
|
992
|
-
this.items.push(item);
|
|
993
|
-
this.recalculateTotal();
|
|
994
|
-
|
|
995
|
-
return Result.ok();
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
```
|
|
999
|
-
|
|
1000
|
-
### Event Design Guidelines
|
|
1001
|
-
|
|
1002
|
-
1. **Use Past Tense for Domain Events**
|
|
1003
|
-
|
|
1004
|
-
```typescript
|
|
1005
|
-
// Good: Past tense, describes what happened
|
|
1006
|
-
class OrderCreatedEvent extends DomainEvent<OrderCreatedData> {}
|
|
1007
|
-
class OrderConfirmedEvent extends DomainEvent<OrderConfirmedData> {}
|
|
1008
|
-
class OrderCancelledEvent extends DomainEvent<OrderCancelledData> {}
|
|
1009
|
-
|
|
1010
|
-
// Avoid: Present tense or imperative
|
|
1011
|
-
class CreateOrderEvent extends DomainEvent<OrderData> {}
|
|
1012
|
-
class ConfirmOrderEvent extends DomainEvent<OrderData> {}
|
|
1013
|
-
```
|
|
1014
|
-
|
|
1015
|
-
2. **Include Sufficient Data in Events**
|
|
1016
|
-
|
|
1017
|
-
```typescript
|
|
1018
|
-
// Good: Rich event data
|
|
1019
|
-
class OrderCreatedEvent extends DomainEvent<{
|
|
1020
|
-
orderId: string;
|
|
1021
|
-
customerId: string;
|
|
1022
|
-
items: Array<{
|
|
1023
|
-
productId: string;
|
|
1024
|
-
quantity: number;
|
|
1025
|
-
price: number;
|
|
1026
|
-
}>;
|
|
1027
|
-
totalAmount: number;
|
|
1028
|
-
currency: string;
|
|
1029
|
-
createdAt: Date;
|
|
1030
|
-
metadata: {
|
|
1031
|
-
channel: string;
|
|
1032
|
-
promotionCode?: string;
|
|
1033
|
-
};
|
|
1034
|
-
}> {}
|
|
1035
|
-
|
|
1036
|
-
// Avoid: Minimal event data
|
|
1037
|
-
class OrderCreatedEvent extends DomainEvent<{
|
|
1038
|
-
orderId: string;
|
|
1039
|
-
}> {}
|
|
1040
|
-
```
|
|
1041
|
-
|
|
1042
|
-
3. **Version Events for Schema Evolution**
|
|
1043
|
-
|
|
1044
|
-
```typescript
|
|
1045
|
-
// Event versioning
|
|
1046
|
-
class OrderCreatedEvent extends DomainEvent<OrderCreatedData> {
|
|
1047
|
-
static readonly VERSION = 2;
|
|
1048
|
-
|
|
1049
|
-
constructor(payload: OrderCreatedData) {
|
|
1050
|
-
super('OrderCreated', payload);
|
|
1051
|
-
this.version = OrderCreatedEvent.VERSION;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
// Event upcasting for version compatibility
|
|
1056
|
-
class OrderCreatedEventUpcaster {
|
|
1057
|
-
upcast(event: IDomainEvent): IDomainEvent {
|
|
1058
|
-
if (event.version === 1) {
|
|
1059
|
-
return this.upcastV1ToV2(event);
|
|
1060
|
-
}
|
|
1061
|
-
return event;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
private upcastV1ToV2(event: IDomainEvent): IDomainEvent {
|
|
1065
|
-
// Convert v1 to v2 format
|
|
1066
|
-
return new OrderCreatedEvent({
|
|
1067
|
-
...event.payload,
|
|
1068
|
-
currency: event.payload.currency || 'USD', // Default currency
|
|
1069
|
-
});
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
```
|
|
1073
|
-
|
|
1074
|
-
### Testing Strategies
|
|
1075
|
-
|
|
1076
|
-
1. **Unit Test Aggregate Behavior**
|
|
1077
|
-
|
|
1078
|
-
```typescript
|
|
1079
|
-
describe('OrderAggregate', () => {
|
|
1080
|
-
it('should create order with items', () => {
|
|
1081
|
-
// Arrange
|
|
1082
|
-
const customerId = EntityId.create();
|
|
1083
|
-
const item = new OrderItem(EntityId.create(), 2, 29.99);
|
|
1084
|
-
|
|
1085
|
-
// Act
|
|
1086
|
-
const order = OrderAggregate.create(customerId);
|
|
1087
|
-
const result = order.addItem(item);
|
|
1088
|
-
|
|
1089
|
-
// Assert
|
|
1090
|
-
expect(result.isSuccess()).toBe(true);
|
|
1091
|
-
expect(order.getItems()).toHaveLength(1);
|
|
1092
|
-
expect(order.getTotalAmount()).toBe(59.98);
|
|
1093
|
-
|
|
1094
|
-
// Verify events
|
|
1095
|
-
const events = order.getUncommittedEvents();
|
|
1096
|
-
expect(events).toHaveLength(2);
|
|
1097
|
-
expect(events[0]).toBeInstanceOf(OrderCreatedEvent);
|
|
1098
|
-
expect(events[1]).toBeInstanceOf(OrderItemAddedEvent);
|
|
1099
|
-
});
|
|
1100
|
-
|
|
1101
|
-
it('should fail to add item with negative quantity', () => {
|
|
1102
|
-
// Arrange
|
|
1103
|
-
const order = OrderAggregate.create(EntityId.create());
|
|
1104
|
-
const item = new OrderItem(EntityId.create(), -1, 29.99);
|
|
1105
|
-
|
|
1106
|
-
// Act
|
|
1107
|
-
const result = order.addItem(item);
|
|
1108
|
-
|
|
1109
|
-
// Assert
|
|
1110
|
-
expect(result.isFailure()).toBe(true);
|
|
1111
|
-
expect(result.error?.message).toBe('Quantity must be positive');
|
|
1112
|
-
expect(order.getItems()).toHaveLength(0);
|
|
1113
|
-
});
|
|
1114
|
-
});
|
|
1115
|
-
```
|
|
1116
|
-
|
|
1117
|
-
2. **Test Event Sourcing**
|
|
1118
|
-
|
|
1119
|
-
```typescript
|
|
1120
|
-
describe('BankAccountAggregate Event Sourcing', () => {
|
|
1121
|
-
it('should reconstitute from event stream', () => {
|
|
1122
|
-
// Arrange
|
|
1123
|
-
const events = [
|
|
1124
|
-
new AccountOpenedEvent({
|
|
1125
|
-
accountId: 'account-123',
|
|
1126
|
-
initialDeposit: 1000,
|
|
1127
|
-
overdraftLimit: 500,
|
|
1128
|
-
}),
|
|
1129
|
-
new MoneyDepositedEvent({
|
|
1130
|
-
accountId: 'account-123',
|
|
1131
|
-
amount: 250,
|
|
1132
|
-
balance: 1250,
|
|
1133
|
-
}),
|
|
1134
|
-
new MoneyWithdrawnEvent({
|
|
1135
|
-
accountId: 'account-123',
|
|
1136
|
-
amount: 100,
|
|
1137
|
-
balance: 1150,
|
|
1138
|
-
}),
|
|
1139
|
-
];
|
|
1140
|
-
|
|
1141
|
-
// Act
|
|
1142
|
-
const account = BankAccountAggregate.fromEventStream(events);
|
|
1143
|
-
|
|
1144
|
-
// Assert
|
|
1145
|
-
expect(account.getBalance()).toBe(1150);
|
|
1146
|
-
expect(account.getOverdraftLimit()).toBe(500);
|
|
1147
|
-
expect(account.getStatus()).toBe(AccountStatus.ACTIVE);
|
|
1148
|
-
});
|
|
1149
|
-
});
|
|
1150
|
-
```
|
|
1151
|
-
|
|
1152
|
-
3. **Integration Testing with Repository**
|
|
1153
|
-
|
|
1154
|
-
```typescript
|
|
1155
|
-
describe('OrderRepository Integration', () => {
|
|
1156
|
-
it('should save and retrieve aggregate', async () => {
|
|
1157
|
-
// Arrange
|
|
1158
|
-
const order = OrderAggregate.create(EntityId.create());
|
|
1159
|
-
order.addItem(new OrderItem(EntityId.create(), 1, 99.99));
|
|
1160
|
-
|
|
1161
|
-
// Act
|
|
1162
|
-
await orderRepository.save(order);
|
|
1163
|
-
const retrievedOrder = await orderRepository.findById(order.id);
|
|
1164
|
-
|
|
1165
|
-
// Assert
|
|
1166
|
-
expect(retrievedOrder).toBeDefined();
|
|
1167
|
-
expect(retrievedOrder?.getTotalAmount()).toBe(99.99);
|
|
1168
|
-
expect(retrievedOrder?.getItems()).toHaveLength(1);
|
|
1169
|
-
});
|
|
1170
|
-
});
|
|
1171
|
-
```
|
|
1172
|
-
|
|
1173
|
-
## ⚡ Performance
|
|
1174
|
-
|
|
1175
|
-
### Optimization Strategies
|
|
1176
|
-
|
|
1177
|
-
1. **Snapshot Strategy**
|
|
1178
|
-
|
|
1179
|
-
```typescript
|
|
1180
|
-
class OrderRepository {
|
|
1181
|
-
private readonly snapshotThreshold = 20;
|
|
1182
|
-
|
|
1183
|
-
async save(order: OrderAggregate): Promise<void> {
|
|
1184
|
-
// Always save current state
|
|
1185
|
-
await this.saveSnapshot(order);
|
|
1186
|
-
|
|
1187
|
-
// Save events
|
|
1188
|
-
const events = order.getUncommittedEvents();
|
|
1189
|
-
await this.eventStore.append(order.id.value, events);
|
|
1190
|
-
|
|
1191
|
-
// Create snapshot if threshold reached
|
|
1192
|
-
const eventCount = await this.eventStore.getEventCount(order.id.value);
|
|
1193
|
-
if (eventCount >= this.snapshotThreshold) {
|
|
1194
|
-
await this.snapshotStore.createSnapshot(
|
|
1195
|
-
order.id.value,
|
|
1196
|
-
order.toSnapshot()
|
|
1197
|
-
);
|
|
1198
|
-
|
|
1199
|
-
// Optional: Archive old events
|
|
1200
|
-
await this.eventStore.archiveEvents(order.id.value, eventCount - 10);
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
```
|
|
1205
|
-
|
|
1206
|
-
2. **Lazy Loading**
|
|
1207
|
-
|
|
1208
|
-
```typescript
|
|
1209
|
-
class OrderAggregate extends AggregateRoot {
|
|
1210
|
-
private _items: OrderItem[] | null = null;
|
|
1211
|
-
|
|
1212
|
-
getItems(): OrderItem[] {
|
|
1213
|
-
if (this._items === null) {
|
|
1214
|
-
this._items = this.loadItemsFromSnapshot();
|
|
1215
|
-
}
|
|
1216
|
-
return this._items;
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
private loadItemsFromSnapshot(): OrderItem[] {
|
|
1220
|
-
// Load from snapshot or reconstruct from events
|
|
1221
|
-
return this.snapshotData.items.map(item => OrderItem.fromJSON(item));
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
```
|
|
1225
|
-
|
|
1226
|
-
3. **Batching Operations**
|
|
1227
|
-
|
|
1228
|
-
```typescript
|
|
1229
|
-
class OrderAggregate extends AggregateRoot {
|
|
1230
|
-
addItems(items: OrderItem[]): Result<void, Error> {
|
|
1231
|
-
// Validate all items first
|
|
1232
|
-
for (const item of items) {
|
|
1233
|
-
const validation = this.validateItem(item);
|
|
1234
|
-
if (validation.isFailure()) {
|
|
1235
|
-
return validation;
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
// Apply all items in batch
|
|
1240
|
-
for (const item of items) {
|
|
1241
|
-
this.items.push(item);
|
|
1242
|
-
this.totalAmount += item.quantity * item.price;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
// Single event for batch operation
|
|
1246
|
-
this.addDomainEvent(
|
|
1247
|
-
new OrderItemsBatchAddedEvent({
|
|
1248
|
-
orderId: this.id.value,
|
|
1249
|
-
items: items.map(item => item.toJSON()),
|
|
1250
|
-
newTotal: this.totalAmount,
|
|
1251
|
-
})
|
|
1252
|
-
);
|
|
1253
|
-
|
|
1254
|
-
return Result.ok();
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
```
|
|
1258
|
-
|
|
1259
|
-
### Memory Management
|
|
1260
|
-
|
|
1261
|
-
```typescript
|
|
1262
|
-
class AggregateRoot {
|
|
1263
|
-
private eventCache: Map<string, IDomainEvent> = new Map();
|
|
1264
|
-
private readonly maxCacheSize = 1000;
|
|
1265
|
-
|
|
1266
|
-
addDomainEvent(event: IDomainEvent): void {
|
|
1267
|
-
// Manage cache size
|
|
1268
|
-
if (this.eventCache.size >= this.maxCacheSize) {
|
|
1269
|
-
// Remove oldest events
|
|
1270
|
-
const oldestKey = this.eventCache.keys().next().value;
|
|
1271
|
-
this.eventCache.delete(oldestKey);
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
this.eventCache.set(event.id, event);
|
|
122
|
+
// Use Entity for inner aggregate objects like OrderLine, Address
|
|
123
|
+
class OrderLine extends Entity<string> {
|
|
124
|
+
constructor(
|
|
125
|
+
id: EntityId<string>,
|
|
126
|
+
private sku: string,
|
|
127
|
+
private quantity: number
|
|
128
|
+
) {
|
|
129
|
+
super({ id });
|
|
1275
130
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
return Array.from(this.eventCache.values());
|
|
131
|
+
getSku(): string {
|
|
132
|
+
return this.sku;
|
|
1279
133
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
this.eventCache.clear();
|
|
134
|
+
getQuantity(): number {
|
|
135
|
+
return this.quantity;
|
|
1283
136
|
}
|
|
1284
137
|
}
|
|
1285
138
|
```
|
|
1286
139
|
|
|
1287
|
-
##
|
|
1288
|
-
|
|
1289
|
-
### Aggregate Testing Utilities
|
|
1290
|
-
|
|
1291
|
-
```typescript
|
|
1292
|
-
import { AggregateTestBuilder } from '@vytches/ddd-aggregates/testing';
|
|
1293
|
-
|
|
1294
|
-
// Aggregate builder for tests
|
|
1295
|
-
const orderBuilder = new AggregateTestBuilder(OrderAggregate)
|
|
1296
|
-
.withCustomer('customer-123')
|
|
1297
|
-
.withItems([
|
|
1298
|
-
{ productId: 'product-1', quantity: 2, price: 29.99 },
|
|
1299
|
-
{ productId: 'product-2', quantity: 1, price: 49.99 },
|
|
1300
|
-
])
|
|
1301
|
-
.withStatus(OrderStatus.PENDING)
|
|
1302
|
-
.build();
|
|
1303
|
-
|
|
1304
|
-
// Event stream testing
|
|
1305
|
-
const account = new AggregateTestBuilder(BankAccountAggregate)
|
|
1306
|
-
.fromEventStream([
|
|
1307
|
-
new AccountOpenedEvent({ accountId: 'account-123', initialDeposit: 1000 }),
|
|
1308
|
-
new MoneyDepositedEvent({
|
|
1309
|
-
accountId: 'account-123',
|
|
1310
|
-
amount: 500,
|
|
1311
|
-
balance: 1500,
|
|
1312
|
-
}),
|
|
1313
|
-
])
|
|
1314
|
-
.build();
|
|
1315
|
-
|
|
1316
|
-
// Snapshot testing
|
|
1317
|
-
const order = new AggregateTestBuilder(OrderAggregate)
|
|
1318
|
-
.fromSnapshot({
|
|
1319
|
-
id: 'order-123',
|
|
1320
|
-
version: 5,
|
|
1321
|
-
data: {
|
|
1322
|
-
/* snapshot data */
|
|
1323
|
-
},
|
|
1324
|
-
})
|
|
1325
|
-
.build();
|
|
1326
|
-
```
|
|
1327
|
-
|
|
1328
|
-
### Event Testing
|
|
1329
|
-
|
|
1330
|
-
```typescript
|
|
1331
|
-
import { EventTestHarness } from '@vytches/ddd-aggregates/testing';
|
|
1332
|
-
|
|
1333
|
-
describe('Order Events', () => {
|
|
1334
|
-
let eventHarness: EventTestHarness;
|
|
1335
|
-
|
|
1336
|
-
beforeEach(() => {
|
|
1337
|
-
eventHarness = new EventTestHarness();
|
|
1338
|
-
});
|
|
1339
|
-
|
|
1340
|
-
it('should emit correct events when adding item', () => {
|
|
1341
|
-
// Arrange
|
|
1342
|
-
const order = OrderAggregate.create(EntityId.create());
|
|
1343
|
-
const item = new OrderItem(EntityId.create(), 1, 99.99);
|
|
1344
|
-
|
|
1345
|
-
// Act
|
|
1346
|
-
order.addItem(item);
|
|
1347
|
-
|
|
1348
|
-
// Assert
|
|
1349
|
-
eventHarness.assertEventEmitted(order, OrderItemAddedEvent, {
|
|
1350
|
-
orderId: order.id.value,
|
|
1351
|
-
productId: item.productId.value,
|
|
1352
|
-
quantity: 1,
|
|
1353
|
-
price: 99.99,
|
|
1354
|
-
});
|
|
1355
|
-
});
|
|
1356
|
-
});
|
|
1357
|
-
```
|
|
1358
|
-
|
|
1359
|
-
## 🤝 Contributing
|
|
140
|
+
## API Reference
|
|
1360
141
|
|
|
1361
|
-
|
|
1362
|
-
[Contributing Guide](../../CONTRIBUTING.md) for details.
|
|
142
|
+
### Core Classes
|
|
1363
143
|
|
|
1364
|
-
|
|
144
|
+
| Export | Kind | Description |
|
|
145
|
+
| -------------------- | -------- | ------------------------------------------------------------------------------------------------------ |
|
|
146
|
+
| `AggregateRoot<TId>` | class | Base aggregate root; provides identity, versioning, event collection and event sourcing reconstitution |
|
|
147
|
+
| `Entity<TId>` | class | Base class for non-root domain entities; identity-based equality, no events |
|
|
148
|
+
| `AggregateBuilder` | class | Fluent builder for aggregates with capabilities |
|
|
149
|
+
| `aggregateBuilder` | function | Factory function shorthand for `AggregateBuilder` |
|
|
150
|
+
| `AggregateError` | class | Error thrown for aggregate-specific failures |
|
|
1365
151
|
|
|
1366
|
-
|
|
1367
|
-
# Clone repository
|
|
1368
|
-
git clone https://github.com/vytches/ddd.git
|
|
1369
|
-
cd ddd
|
|
1370
|
-
|
|
1371
|
-
# Install dependencies
|
|
1372
|
-
pnpm install
|
|
1373
|
-
|
|
1374
|
-
# Build aggregates package
|
|
1375
|
-
pnpm build --filter=@vytches/ddd-aggregates
|
|
1376
|
-
|
|
1377
|
-
# Run tests
|
|
1378
|
-
pnpm test --filter=@vytches/ddd-aggregates
|
|
1379
|
-
|
|
1380
|
-
# Run in development mode
|
|
1381
|
-
pnpm dev --filter=@vytches/ddd-aggregates
|
|
1382
|
-
```
|
|
1383
|
-
|
|
1384
|
-
## 📄 License
|
|
1385
|
-
|
|
1386
|
-
This project is licensed under the MIT License - see the
|
|
1387
|
-
[LICENSE](../../LICENSE) file for details.
|
|
1388
|
-
|
|
1389
|
-
---
|
|
1390
|
-
|
|
1391
|
-
**Part of the [@vytches/ddd-core](https://github.com/vytches/ddd) ecosystem**
|
|
152
|
+
### Capabilities
|
|
1392
153
|
|
|
1393
|
-
|
|
154
|
+
| Export | Kind | Description |
|
|
155
|
+
| ------------------------- | ----- | -------------------------------------------------------- |
|
|
156
|
+
| `AuditCapability` | class | Records who created/modified the aggregate and when |
|
|
157
|
+
| `EventSourcingCapability` | class | Enables event sourcing reconstitution from stored events |
|
|
158
|
+
| `SnapshotCapability` | class | Enables taking and restoring state snapshots |
|
|
159
|
+
| `VersioningCapability` | class | Manages optimistic concurrency version tracking |
|
|
160
|
+
|
|
161
|
+
### Capability Utilities
|
|
162
|
+
|
|
163
|
+
| Export | Kind | Description |
|
|
164
|
+
| ---------------------------------- | -------- | ------------------------------------------------------------- |
|
|
165
|
+
| `asSnapshotAggregate(agg)` | function | Cast to snapshot-capable aggregate; throws if missing |
|
|
166
|
+
| `tryAsSnapshotAggregate(agg)` | function | Cast to snapshot-capable aggregate; returns `null` if missing |
|
|
167
|
+
| `asVersioningAggregate(agg)` | function | Cast to versioning-capable aggregate; throws if missing |
|
|
168
|
+
| `tryAsVersioningAggregate(agg)` | function | Returns `null` if versioning capability absent |
|
|
169
|
+
| `asAuditAggregate(agg)` | function | Cast to audit-capable aggregate; throws if missing |
|
|
170
|
+
| `tryAsAuditAggregate(agg)` | function | Returns `null` if audit capability absent |
|
|
171
|
+
| `asEventSourcingAggregate(agg)` | function | Cast to event-sourcing aggregate; throws if missing |
|
|
172
|
+
| `tryAsEventSourcingAggregate(agg)` | function | Returns `null` if event sourcing capability absent |
|
|
173
|
+
| `getAggregateCapabilities(agg)` | function | Returns all capabilities attached to an aggregate |
|
|
174
|
+
| `hasAllCapabilities(agg, caps)` | function | Returns `true` if aggregate has all listed capabilities |
|
|
175
|
+
|
|
176
|
+
### Interfaces
|
|
177
|
+
|
|
178
|
+
| Export | Kind | Description |
|
|
179
|
+
| ---------------------------------- | --------- | ------------------------------------------ |
|
|
180
|
+
| `IAggregateRoot` | interface | Full contract for aggregate roots |
|
|
181
|
+
| `IAggregateCapability` | interface | Base capability contract |
|
|
182
|
+
| `IAggregateConstructorParams<TId>` | interface | Constructor parameter shape for aggregates |
|
|
183
|
+
| `IAggregateEventHandler` | interface | Event handler registration contract |
|
|
184
|
+
| `IAggregateBuilder` | interface | Builder contract |
|
|
185
|
+
|
|
186
|
+
### Utility Types
|
|
187
|
+
|
|
188
|
+
| Export | Kind | Description |
|
|
189
|
+
| -------------------------------------- | ---- | ---------------------------------------------- |
|
|
190
|
+
| `AggregateWithSnapshotCapability` | type | Aggregate narrowed to snapshot-capable shape |
|
|
191
|
+
| `AggregateWithVersioningCapability` | type | Aggregate narrowed to versioning-capable shape |
|
|
192
|
+
| `AggregateWithAuditCapability` | type | Aggregate narrowed to audit-capable shape |
|
|
193
|
+
| `AggregateWithEventSourcingCapability` | type | Aggregate narrowed to event-sourcing shape |
|
|
194
|
+
|
|
195
|
+
## Package boundaries
|
|
196
|
+
|
|
197
|
+
`@vytches/ddd-aggregates` depends on:
|
|
198
|
+
|
|
199
|
+
- `@vytches/ddd-contracts` — base interfaces and `EntityId`
|
|
200
|
+
- `@vytches/ddd-domain-primitives` — error types
|
|
201
|
+
- `@vytches/ddd-value-objects` — enhanced `EntityId`
|
|
202
|
+
- `@vytches/ddd-logging` — internal logging
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT
|