@shirudo/ddd-kit 0.8.8 → 0.9.1

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 CHANGED
@@ -1,43 +1,920 @@
1
1
  # @shirudo/ddd-kit
2
2
 
3
- > Composable TypeScript toolkit for tactical DDD
3
+ Composable TypeScript toolkit for tactical Domain-Driven Design.
4
4
 
5
- ---
5
+ ## Badges
6
6
 
7
- ### ⚠️ Beta Notice
7
+ ![npm version](https://img.shields.io/npm/v/@shirudo/ddd-kit)
8
+ ![license](https://img.shields.io/npm/l/@shirudo/ddd-kit)
8
9
 
9
- This project is in its early stages and should be considered a **beta release**. The API is subject to change, and it is not recommended for use in production environments at this time.
10
+ ## Features
10
11
 
11
- ---
12
+ - **Value Objects** - Immutable objects defined by their attributes, ensuring data integrity
13
+ - **Entities** - Optional interface and helpers for entities with identity, useful for nested entities within aggregates
14
+ - **Aggregates** - Event-sourced aggregates with versioning for optimistic concurrency control
15
+ - **Domain Events** - Type-safe domain events with versioning and metadata for schema evolution and traceability
16
+ - **Repositories** - Persistence abstraction layer for aggregates with specification pattern support
17
+ - **Specifications** - Reusable query specifications for complex domain queries
18
+ - **Unit of Work** - Transaction management for maintaining consistency across operations
19
+ - **Result Type** - Functional error handling with `Result<T, E>` type for explicit success/failure states
12
20
 
13
- ## About
21
+ ## Installation
14
22
 
15
- `@shirudo/ddd-kit` is a lightweight, composable toolkit designed to help developers implement tactical Domain-Driven Design patterns in TypeScript. It provides a set of simple, unopinionated building blocks that you can use to create robust and maintainable domain models.
23
+ Install the package using npm:
16
24
 
17
- ## Getting Started
25
+ ```bash
26
+ npm install @shirudo/ddd-kit
27
+ ```
28
+
29
+ Or using pnpm:
18
30
 
19
- To get a feel for how the toolkit works, you can explore the "Rugby Match" example included in the `examples/rugby` directory.
31
+ ```bash
32
+ pnpm add @shirudo/ddd-kit
33
+ ```
20
34
 
21
- ### Running the Example
35
+ ## Quick Start
22
36
 
23
- To run the example, execute the following command in your terminal:
37
+ Here's a minimal example showing how to create and use a Value Object:
24
38
 
25
- ```bash
26
- npx tsx examples/rugby/main.ts
39
+ ```typescript
40
+ import { vo, type ValueObject } from "@shirudo/ddd-kit";
41
+
42
+ type EmailAddress = ValueObject<{
43
+ value: string;
44
+ }>;
45
+
46
+ function createEmail(value: string): EmailAddress {
47
+ if (!value.includes("@")) {
48
+ throw new Error("Invalid email address");
49
+ }
50
+ return vo({ value });
51
+ }
52
+
53
+ const email = createEmail("user@example.com");
54
+ // email.value is readonly and immutable
27
55
  ```
28
56
 
29
- This will simulate a rugby match, demonstrating how aggregates, events, and state changes work together.
57
+ ## Core Concepts
30
58
 
31
- ### Running Tests
59
+ ### Value Objects
32
60
 
33
- To run the test suite for the project, use the following command:
61
+ Value Objects are immutable objects that are defined by their attributes rather than identity. They ensure data integrity by preventing modification after creation. Use the `vo()` helper function to create deeply frozen value objects that cannot be mutated, even nested objects and arrays. The library provides `voEquals()` for value-based equality comparison, `voWithValidation()` for creating validated value objects (returns Result), and `voWithValidationUnsafe()` for the exception-throwing variant.
34
62
 
35
- ```bash
36
- pnpm test
63
+ ### Entities
64
+
65
+ Entities are objects with unique identity that are defined by their ID rather than their attributes. The optional `Entity<TId>` interface can be used for nested entities within aggregates or entities that are not aggregate roots. Helper functions like `sameEntity()`, `findEntityById()`, and `hasEntityId()` provide utilities for working with entity collections.
66
+
67
+ ### Aggregates
68
+
69
+ Aggregates are clusters of entities and value objects that form a consistency boundary. The library provides:
70
+
71
+ - **`AggregateRoot<TId>`** - Marker interface for Aggregate Roots. Aggregate Roots are the entry points for modifying aggregates in DDD. They have identity (id) and version for optimistic concurrency control. All aggregate base classes implement this interface.
72
+
73
+ - **`AggregateBase<TState, TId>`** - Base class for aggregates without Event Sourcing. Implements `AggregateRoot<TId>`. Provides ID and version management, state management, and snapshot support. Use this when you don't need Event Sourcing but still want aggregate patterns with optimistic concurrency control.
74
+
75
+ - **`AggregateEventSourced<TState, TEvent, TId>`** - Base class for Event-Sourced aggregates. Extends `AggregateBase` (and thus implements `AggregateRoot<TId>`). Adds event tracking, event handlers, event validation, and history replay capabilities. Use this when you want full Event Sourcing with event tracking and replay.
76
+
77
+ Both classes support automatic versioning (configurable), snapshot creation/restoration, and optimistic concurrency control.
78
+
79
+ ### CQRS (Command Query Responsibility Segregation)
80
+
81
+ CQRS separates read operations (Queries) from write operations (Commands), providing clear patterns for handling different types of operations. Commands change system state and return `Result` for error handling, while Queries read data and return results directly. The library provides optional Command and Query Buses for centralized handler registration and execution.
82
+
83
+ ### Domain Events
84
+
85
+ Domain Events represent something meaningful that happened in your domain. They are immutable records with a type, payload, timestamp, optional version for schema evolution, and metadata for traceability. Events support versioning for handling schema changes over time and include metadata fields like `correlationId`, `causationId`, `userId`, and `source` for tracking event flow in distributed systems. Events are automatically tracked by aggregates and can be published to event buses or stored in outboxes for eventual consistency.
86
+
87
+ ### Repositories
88
+
89
+ Repositories abstract the persistence layer, allowing you to work with aggregates without dealing with database specifics. They support finding aggregates by ID, using specifications for complex queries, and saving/deleting aggregates while maintaining transactional boundaries.
90
+
91
+ ### Specifications
92
+
93
+ Specifications encapsulate business rules for queries in a reusable, composable way. They provide a domain-centric approach to querying that separates business logic from data access implementation details.
94
+
95
+ ### Result Type
96
+
97
+ The `Result<T, E>` type provides functional error handling without exceptions. It explicitly represents success (`Ok<T>`) or failure (`Err<E>`) states, making error handling predictable and type-safe throughout your domain logic.
98
+
99
+ ## Usage Examples
100
+
101
+ ### Creating a Value Object
102
+
103
+ ```typescript
104
+ import { vo, voEquals, voWithValidation, type ValueObject } from "@shirudo/ddd-kit";
105
+
106
+ // Simple value object
107
+ type Money = ValueObject<{
108
+ amount: number;
109
+ currency: string;
110
+ }>;
111
+
112
+ const price = vo({ amount: 99.99, currency: "USD" });
113
+ // price is deeply immutable - nested objects and arrays are also frozen
114
+
115
+ // Value object with validation (returns Result)
116
+ const result = voWithValidation(
117
+ { amount: 100, currency: "USD" },
118
+ (m) => m.amount >= 0 && m.currency.length === 3,
119
+ "Amount must be non-negative and currency must be 3 characters"
120
+ );
121
+
122
+ if (result.ok) {
123
+ const validMoney = result.value;
124
+ // Use validMoney...
125
+ } else {
126
+ console.error(result.error);
127
+ }
128
+
129
+ // Or use unsafe variant (throws exception)
130
+ const validMoneyUnsafe = voWithValidationUnsafe(
131
+ { amount: 100, currency: "USD" },
132
+ (m) => m.amount >= 0 && m.currency.length === 3,
133
+ "Amount must be non-negative and currency must be 3 characters"
134
+ );
135
+
136
+ // Value object with nested structures (deep freeze)
137
+ const address = vo({
138
+ street: "Main St",
139
+ city: "Berlin",
140
+ coordinates: { lat: 52.5, lng: 13.4 }
141
+ });
142
+ // address.coordinates.lat = 99; // ❌ Error: Cannot assign to read-only property
143
+
144
+ // Equality comparison
145
+ const money1 = vo({ amount: 100, currency: "USD" });
146
+ const money2 = vo({ amount: 100, currency: "USD" });
147
+ voEquals(money1, money2); // true (value equality, not reference)
148
+ ```
149
+
150
+ ### Creating an Aggregate WITHOUT Event Sourcing
151
+
152
+ ```typescript
153
+ import {
154
+ AggregateBase,
155
+ type AggregateRoot,
156
+ type Id,
157
+ } from "@shirudo/ddd-kit";
158
+
159
+ type OrderId = Id<"OrderId">;
160
+
161
+ type OrderState = {
162
+ id: OrderId;
163
+ customerId: string;
164
+ items: Array<{ productId: string; quantity: number; price: number }>;
165
+ total: number;
166
+ status: "pending" | "confirmed" | "shipped";
167
+ };
168
+
169
+ class Order extends AggregateBase<OrderState, OrderId> implements AggregateRoot<OrderId> {
170
+ static create(id: OrderId, customerId: string): Order {
171
+ const initialState: OrderState = {
172
+ id,
173
+ customerId,
174
+ items: [],
175
+ total: 0,
176
+ status: "pending",
177
+ };
178
+ return new Order(id, initialState);
179
+ }
180
+
181
+ addItem(productId: string, quantity: number, price: number): void {
182
+ if (this._state.status !== "pending") {
183
+ throw new Error("Cannot add items to a non-pending order");
184
+ }
185
+
186
+ this._state = {
187
+ ...this._state,
188
+ items: [...this._state.items, { productId, quantity, price }],
189
+ total: this._state.total + quantity * price,
190
+ };
191
+ this.bumpVersion(); // Manual version bump for optimistic concurrency control
192
+ }
193
+
194
+ confirm(): void {
195
+ if (this._state.status !== "pending") {
196
+ throw new Error("Only pending orders can be confirmed");
197
+ }
198
+ this._state = { ...this._state, status: "confirmed" };
199
+ this.bumpVersion();
200
+ }
201
+
202
+ ship(): void {
203
+ if (this._state.status !== "confirmed") {
204
+ throw new Error("Only confirmed orders can be shipped");
205
+ }
206
+ this._state = { ...this._state, status: "shipped" };
207
+ this.bumpVersion();
208
+ }
209
+ }
210
+
211
+ // Usage
212
+ const order = Order.create("order-123" as OrderId, "customer-456");
213
+ order.addItem("product-1", 2, 10.0);
214
+ order.confirm();
215
+ order.ship();
216
+
217
+ console.log(order.version); // 3 (manually bumped)
218
+ console.log(order.state.status); // "shipped"
219
+ ```
220
+
221
+ ### Creating an Aggregate WITH Event Sourcing
222
+
223
+ ```typescript
224
+ import {
225
+ AggregateEventSourced,
226
+ createDomainEvent,
227
+ type AggregateRoot,
228
+ type Id,
229
+ type DomainEvent,
230
+ } from "@shirudo/ddd-kit";
231
+
232
+ type OrderId = Id<"OrderId">;
233
+
234
+ type OrderState = {
235
+ id: OrderId;
236
+ customerId: string;
237
+ items: string[];
238
+ status: "pending" | "confirmed" | "shipped";
239
+ };
240
+
241
+ type OrderCreated = DomainEvent<"OrderCreated", { customerId: string }>;
242
+ type OrderConfirmed = DomainEvent<"OrderConfirmed", {}>;
243
+ type OrderShipped = DomainEvent<"OrderShipped", { trackingNumber: string }>;
244
+
245
+ type OrderEvent = OrderCreated | OrderConfirmed | OrderShipped;
246
+
247
+ class Order extends AggregateEventSourced<OrderState, OrderEvent, OrderId> implements AggregateRoot<OrderId> {
248
+ static create(id: OrderId, customerId: string): Order {
249
+ const initialState: OrderState = {
250
+ id,
251
+ customerId,
252
+ items: [],
253
+ status: "pending",
254
+ };
255
+ const order = new Order(id, initialState);
256
+ order.apply(
257
+ createDomainEvent("OrderCreated", { customerId }) as OrderCreated
258
+ );
259
+ return order;
260
+ }
261
+
262
+ confirm(): void {
263
+ const result = this.apply(
264
+ createDomainEvent("OrderConfirmed", {}) as OrderConfirmed
265
+ );
266
+ if (!result.ok) {
267
+ throw new Error(result.error);
268
+ }
269
+ }
270
+
271
+ ship(trackingNumber: string): void {
272
+ const result = this.apply(
273
+ createDomainEvent("OrderShipped", { trackingNumber }) as OrderShipped
274
+ );
275
+ if (!result.ok) {
276
+ throw new Error(result.error);
277
+ }
278
+ }
279
+
280
+ // Or use unsafe variant (throws exception directly)
281
+ confirmUnsafe(): void {
282
+ this.applyUnsafe(
283
+ createDomainEvent("OrderConfirmed", {}) as OrderConfirmed
284
+ );
285
+ }
286
+
287
+ protected readonly handlers = {
288
+ OrderCreated: (state: OrderState, event: OrderCreated): OrderState => ({
289
+ ...state,
290
+ customerId: event.payload.customerId,
291
+ status: "pending",
292
+ }),
293
+ OrderConfirmed: (state: OrderState): OrderState => ({
294
+ ...state,
295
+ status: "confirmed",
296
+ }),
297
+ OrderShipped: (state: OrderState, event: OrderShipped): OrderState => ({
298
+ ...state,
299
+ status: "shipped",
300
+ }),
301
+ };
302
+ }
303
+
304
+ // Usage
305
+ const orderId = "order-123" as OrderId;
306
+ const order = Order.create(orderId, "customer-456");
307
+ order.confirm();
308
+ order.ship("TRACK-789");
309
+
310
+ // Access pending events
311
+ console.log(order.pendingEvents); // Array of events not yet persisted
312
+
313
+ // Helper methods
314
+ console.log(order.hasPendingEvents()); // true
315
+ console.log(order.getEventCount()); // 3
316
+ console.log(order.getLatestEvent()?.type); // "OrderShipped"
317
+ console.log(order.version); // 3 (automatically bumped)
318
+ ```
319
+
320
+ ### Aggregate Features: Snapshots and Configuration
321
+
322
+ ```typescript
323
+ import {
324
+ AggregateBase,
325
+ AggregateEventSourced,
326
+ sameAggregate,
327
+ type Id,
328
+ } from "@shirudo/ddd-kit";
329
+
330
+ type OrderId = Id<"OrderId">;
331
+ type OrderState = { id: OrderId; status: "pending" | "confirmed" | "shipped" };
332
+
333
+ // Snapshots work with both aggregate types
334
+ const order = Order.create("order-123" as OrderId, "customer-456");
335
+ order.confirm();
336
+
337
+ const snapshot = order.createSnapshot();
338
+ // Save snapshot to database...
339
+
340
+ // Later: restore from snapshot (without events)
341
+ const restoredOrder = Order.create("order-123" as OrderId, "customer-456");
342
+ restoredOrder.restoreFromSnapshot(snapshot);
343
+
344
+ // For Event-Sourced aggregates: restore with events after snapshot
345
+ const eventSourcedOrder = EventSourcedOrder.create("order-123" as OrderId, "customer-456");
346
+ const eventsAfterSnapshot = [/* events that occurred after snapshot */];
347
+ eventSourcedOrder.restoreFromSnapshotWithEvents(snapshot, eventsAfterSnapshot);
348
+
349
+ // Aggregate equality check
350
+ const order1 = await repository.getById(id);
351
+ // ... some operations ...
352
+ const order2 = await repository.getById(id);
353
+ if (!sameAggregate(order1, order2)) {
354
+ throw new Error("Aggregate was modified by another process");
355
+ }
356
+ ```
357
+
358
+ ### Event Validation (Event-Sourced Aggregates Only)
359
+
360
+ ```typescript
361
+ import {
362
+ AggregateEventSourced,
363
+ createDomainEvent,
364
+ err,
365
+ ok,
366
+ type AggregateRoot,
367
+ type Id,
368
+ type DomainEvent,
369
+ type Result,
370
+ } from "@shirudo/ddd-kit";
371
+
372
+ type OrderId = Id<"OrderId">;
373
+ type OrderState = { id: OrderId; status: "pending" | "confirmed" | "shipped" };
374
+ type OrderShipped = DomainEvent<"OrderShipped", { trackingNumber: string }>;
375
+ type OrderEvent = OrderShipped;
376
+
377
+ class Order extends AggregateEventSourced<OrderState, OrderEvent, OrderId> implements AggregateRoot<OrderId> {
378
+ // Event validation
379
+ protected validateEvent(event: OrderEvent): Result<true, string> {
380
+ if (event.type === "OrderShipped" && this.state.status !== "confirmed") {
381
+ return err("Order must be confirmed before shipping");
382
+ }
383
+ return ok(true);
384
+ }
385
+
386
+ ship(trackingNumber: string): void {
387
+ this.apply(
388
+ createDomainEvent("OrderShipped", { trackingNumber }) as OrderShipped
389
+ );
390
+ }
391
+
392
+ protected readonly handlers = {
393
+ OrderShipped: (state: OrderState, event: OrderShipped): OrderState => ({
394
+ ...state,
395
+ status: "shipped",
396
+ }),
397
+ };
398
+ }
399
+ ```
400
+
401
+ ### Using CQRS: Commands and Queries
402
+
403
+ #### Commands (Write Operations)
404
+
405
+ Commands represent write operations that change system state. They return `Result` for explicit error handling.
406
+
407
+ ```typescript
408
+ import {
409
+ Command,
410
+ CommandHandler,
411
+ CommandBus,
412
+ ok,
413
+ err,
414
+ type Result,
415
+ } from "@shirudo/ddd-kit";
416
+
417
+ // Define a command
418
+ type CreateOrderCommand = Command & {
419
+ type: "CreateOrder";
420
+ customerId: string;
421
+ items: Array<{ productId: string; quantity: number }>;
422
+ };
423
+
424
+ // Create a command handler
425
+ const createOrderHandler: CommandHandler<CreateOrderCommand, string> = async (
426
+ cmd
427
+ ) => {
428
+ // Validate input
429
+ if (cmd.items.length === 0) {
430
+ return err("Order must have at least one item");
431
+ }
432
+
433
+ // Perform business logic
434
+ const order = Order.create(cmd.customerId, cmd.items);
435
+ await repository.save(order);
436
+
437
+ return ok(order.id);
438
+ };
439
+
440
+ // Use directly
441
+ const result = await createOrderHandler({
442
+ type: "CreateOrder",
443
+ customerId: "customer-123",
444
+ items: [{ productId: "product-1", quantity: 2 }],
445
+ });
446
+
447
+ if (result.ok) {
448
+ console.log("Order created:", result.value);
449
+ } else {
450
+ console.error("Error:", result.error);
451
+ }
452
+
453
+ // Or use with Command Bus (basic in-memory implementation)
454
+ // Note: For production, consider using external buses (RabbitMQ, AWS SQS) with typed handlers
455
+ const commandBus = new CommandBus();
456
+ commandBus.register("CreateOrder", createOrderHandler);
457
+
458
+ const busResult = await commandBus.execute({
459
+ type: "CreateOrder",
460
+ customerId: "customer-123",
461
+ items: [{ productId: "product-1", quantity: 2 }],
462
+ });
37
463
  ```
38
464
 
39
- This will execute all tests and provide a report of the results.
465
+ #### Queries (Read Operations)
466
+
467
+ Queries represent read operations that don't change system state. They return data directly.
468
+
469
+ ```typescript
470
+ import {
471
+ Query,
472
+ QueryHandler,
473
+ QueryBus,
474
+ } from "@shirudo/ddd-kit";
475
+
476
+ // Define a query
477
+ type GetOrderQuery = Query & {
478
+ type: "GetOrder";
479
+ orderId: string;
480
+ };
481
+
482
+ // Create a query handler
483
+ const getOrderHandler: QueryHandler<GetOrderQuery, Order | null> = async (
484
+ query
485
+ ) => {
486
+ return await repository.getById(query.orderId);
487
+ };
488
+
489
+ // Use directly
490
+ const order = await getOrderHandler({
491
+ type: "GetOrder",
492
+ orderId: "order-123",
493
+ });
494
+
495
+ // Or use with Query Bus (basic in-memory implementation)
496
+ // Note: For production, consider using external buses (RabbitMQ, AWS SQS) with typed handlers
497
+ const queryBus = new QueryBus();
498
+ queryBus.register("GetOrder", getOrderHandler);
499
+
500
+ // Safe variant (returns Result)
501
+ const result = await queryBus.execute({
502
+ type: "GetOrder",
503
+ orderId: "order-123",
504
+ });
505
+
506
+ if (result.ok) {
507
+ const orderFromBus = result.value;
508
+ // Use orderFromBus...
509
+ } else {
510
+ console.error(result.error);
511
+ }
512
+
513
+ // Or use unsafe variant (throws exception)
514
+ const orderFromBusUnsafe = await queryBus.executeUnsafe({
515
+ type: "GetOrder",
516
+ orderId: "order-123",
517
+ });
518
+ ```
519
+
520
+ #### Combining Commands with Transactions
521
+
522
+ ```typescript
523
+ import { withCommit } from "@shirudo/ddd-kit";
524
+
525
+ const createOrderHandler: CommandHandler<CreateOrderCommand, string> = async (
526
+ cmd
527
+ ) => {
528
+ return await withCommit(
529
+ { outbox, bus, uow },
530
+ async () => {
531
+ const order = Order.create(cmd.customerId, cmd.items);
532
+ await repository.save(order);
533
+
534
+ return {
535
+ result: order.id,
536
+ events: order.pendingEvents,
537
+ };
538
+ }
539
+ );
540
+ };
541
+ ```
542
+
543
+ #### Using Commands/Queries with External Frameworks
544
+
545
+ The `Command` and `Query` interfaces, along with `CommandHandler` and `QueryHandler` types, can be used as type markers even when using external frameworks like RabbitMQ, AWS SQS, or Kafka. This ensures type safety across different bus implementations.
546
+
547
+ **Important:** The included `CommandBus` and `QueryBus` are basic in-memory implementations suitable for development and simple use cases. For production environments, use external production-grade message buses (RabbitMQ, AWS SQS, Kafka, etc.) with typed handlers to get features like:
548
+ - Middleware/Pipeline support (logging, validation, authorization)
549
+ - Error handling and retry logic
550
+ - Timeout handling
551
+ - Metrics and observability
552
+ - Dead letter queues
553
+ - Transaction management
554
+
555
+ ```typescript
556
+ import {
557
+ Command,
558
+ CommandHandler,
559
+ Query,
560
+ QueryHandler,
561
+ ok,
562
+ type Result,
563
+ } from "@shirudo/ddd-kit";
564
+
565
+ // Define commands/queries using marker interfaces
566
+ type CreateOrderCommand = Command & {
567
+ type: "CreateOrder";
568
+ customerId: string;
569
+ items: OrderItem[];
570
+ };
571
+
572
+ type GetOrderQuery = Query & {
573
+ type: "GetOrder";
574
+ orderId: OrderId;
575
+ };
576
+
577
+ // Handler typed with CommandHandler for type safety
578
+ const createOrderHandler: CommandHandler<CreateOrderCommand, OrderId> = async (
579
+ cmd
580
+ ) => {
581
+ const order = Order.create(cmd.customerId, cmd.items);
582
+ await repository.save(order);
583
+ return ok(order.id);
584
+ };
585
+
586
+ // Handler typed with QueryHandler for type safety
587
+ const getOrderHandler: QueryHandler<GetOrderQuery, Order | null> = async (
588
+ query
589
+ ) => {
590
+ return await repository.getById(query.orderId);
591
+ };
592
+
593
+ // Use with RabbitMQ (or any external framework)
594
+ import amqp from "amqplib";
595
+
596
+ const connection = await amqp.connect("amqp://localhost");
597
+ const channel = await connection.createChannel();
598
+
599
+ // Command handler for RabbitMQ
600
+ channel.consume("order.commands", async (message) => {
601
+ if (!message) return;
602
+
603
+ const command = JSON.parse(message.content.toString()) as CreateOrderCommand;
604
+ const result = await createOrderHandler(command);
605
+
606
+ if (result.ok) {
607
+ channel.ack(message);
608
+ } else {
609
+ channel.nack(message, false, true); // Requeue on error
610
+ }
611
+ });
612
+
613
+ // Query handler for RabbitMQ
614
+ channel.consume("order.queries", async (message) => {
615
+ if (!message) return;
616
+
617
+ const query = JSON.parse(message.content.toString()) as GetOrderQuery;
618
+ const result = await getOrderHandler(query);
619
+
620
+ channel.sendToQueue(
621
+ message.properties.replyTo,
622
+ Buffer.from(JSON.stringify(result)),
623
+ { correlationId: message.properties.correlationId }
624
+ );
625
+ channel.ack(message);
626
+ });
627
+
628
+ // Same handlers work with AWS SQS, Kafka, etc.
629
+ ```
630
+
631
+ ### Using Event Bus for Event Handling
632
+
633
+ The Event Bus provides a pub/sub pattern for handling domain events. Multiple handlers can subscribe to the same event type.
634
+
635
+ ```typescript
636
+ import {
637
+ EventBusImpl,
638
+ createDomainEvent,
639
+ type DomainEvent,
640
+ } from "@shirudo/ddd-kit";
641
+
642
+ type OrderCreated = DomainEvent<"OrderCreated", { orderId: string; customerId: string }>;
643
+ type OrderEvent = OrderCreated;
644
+
645
+ // Create event bus
646
+ const eventBus = new EventBusImpl<OrderEvent>();
647
+
648
+ // Subscribe handlers to events
649
+ eventBus.subscribe("OrderCreated", async (event) => {
650
+ await sendEmail(event.payload.customerId);
651
+ });
652
+
653
+ eventBus.subscribe("OrderCreated", async (event) => {
654
+ await logEvent(event);
655
+ });
656
+
657
+ // Unsubscribe if needed
658
+ const unsubscribe = eventBus.subscribe("OrderCreated", async (event) => {
659
+ console.log("Order created:", event.payload.orderId);
660
+ });
661
+ // Later: unsubscribe();
662
+
663
+ // Publish events (all subscribed handlers will be called)
664
+ const orderCreated = createDomainEvent("OrderCreated", {
665
+ orderId: "order-123",
666
+ customerId: "customer-456",
667
+ }) as OrderCreated;
668
+
669
+ await eventBus.publish([orderCreated]);
670
+ // Both email and logging handlers will be called
671
+ ```
672
+
673
+ ### Creating Events with Metadata for Traceability
674
+
675
+ ```typescript
676
+ import {
677
+ createDomainEventWithMetadata,
678
+ copyMetadata,
679
+ type EventMetadata,
680
+ } from "@shirudo/ddd-kit";
681
+
682
+ // Create event with metadata for distributed tracing
683
+ const orderCreated = createDomainEventWithMetadata(
684
+ "OrderCreated",
685
+ { orderId: "123", customerId: "cust-456" },
686
+ {
687
+ correlationId: "corr-123", // Trace across services
688
+ causationId: "cmd-456", // Parent command/event
689
+ userId: "user-789", // Who triggered it
690
+ source: "order-service", // Service name
691
+ }
692
+ );
693
+
694
+ // Create follow-up event maintaining correlation chain
695
+ const orderShipped = createDomainEventWithMetadata(
696
+ "OrderShipped",
697
+ { orderId: "123", trackingNumber: "TRACK-789" },
698
+ copyMetadata(orderCreated, {
699
+ causationId: orderCreated.type, // New causation
700
+ })
701
+ );
702
+
703
+ // Events support versioning for schema evolution
704
+ const eventV1 = createDomainEvent("OrderCreated", { orderId: "123" }, {
705
+ version: 1,
706
+ });
707
+
708
+ const eventV2 = createDomainEvent(
709
+ "OrderCreated",
710
+ { orderId: "123", customerId: "cust-456" }, // Additional field
711
+ { version: 2 }
712
+ );
713
+ ```
714
+
715
+ ### Working with Nested Entities
716
+
717
+ ```typescript
718
+ import {
719
+ AggregateBase,
720
+ createDomainEvent,
721
+ Entity,
722
+ findEntityById,
723
+ hasEntityId,
724
+ removeEntityById,
725
+ sameEntity,
726
+ type Id,
727
+ type DomainEvent,
728
+ } from "@shirudo/ddd-kit";
729
+
730
+ type OrderId = Id<"OrderId">;
731
+ type ItemId = Id<"ItemId">;
732
+
733
+ // Define nested entity
734
+ type OrderItem = Entity<ItemId> & {
735
+ productId: string;
736
+ quantity: number;
737
+ price: number;
738
+ };
739
+
740
+ type OrderState = {
741
+ id: OrderId;
742
+ customerId: string;
743
+ items: OrderItem[];
744
+ total: number;
745
+ };
746
+
747
+ type ItemAdded = DomainEvent<"ItemAdded", { item: OrderItem }>;
748
+ type ItemRemoved = DomainEvent<"ItemRemoved", { itemId: ItemId }>;
749
+ type OrderEvent = ItemAdded | ItemRemoved;
750
+
751
+ class Order extends AggregateBase<OrderState, OrderEvent, OrderId> {
752
+ static create(id: OrderId, customerId: string): Order {
753
+ const initialState: OrderState = {
754
+ id,
755
+ customerId,
756
+ items: [],
757
+ total: 0,
758
+ };
759
+ const order = new Order(id, initialState);
760
+ const result = order.apply(createDomainEvent("OrderCreated", { customerId }) as any);
761
+ if (!result.ok) {
762
+ throw new Error(result.error);
763
+ }
764
+ return order;
765
+ }
766
+
767
+ addItem(item: OrderItem): void {
768
+ if (hasEntityId(this.state.items, item.id)) {
769
+ throw new Error("Item already exists");
770
+ }
771
+ const result = this.apply(createDomainEvent("ItemAdded", { item }) as ItemAdded);
772
+ if (!result.ok) {
773
+ throw new Error(result.error);
774
+ }
775
+ }
776
+
777
+ removeItem(itemId: ItemId): void {
778
+ if (!hasEntityId(this.state.items, itemId)) {
779
+ throw new Error("Item not found");
780
+ }
781
+ const result = this.apply(createDomainEvent("ItemRemoved", { itemId }) as ItemRemoved);
782
+ if (!result.ok) {
783
+ throw new Error(result.error);
784
+ }
785
+ }
786
+
787
+ getItem(itemId: ItemId): OrderItem | undefined {
788
+ return findEntityById(this.state.items, itemId);
789
+ }
790
+
791
+ protected readonly handlers = {
792
+ ItemAdded: (state: OrderState, event: ItemAdded): OrderState => ({
793
+ ...state,
794
+ items: [...state.items, event.payload.item],
795
+ total: state.total + event.payload.item.price * event.payload.item.quantity,
796
+ }),
797
+ ItemRemoved: (state: OrderState, event: ItemRemoved): OrderState => {
798
+ const item = findEntityById(state.items, event.payload.itemId);
799
+ if (!item) return state;
800
+ return {
801
+ ...state,
802
+ items: removeEntityById(state.items, event.payload.itemId),
803
+ total: state.total - item.price * item.quantity,
804
+ };
805
+ },
806
+ };
807
+ }
808
+
809
+ // Usage
810
+ const orderId = "order-123" as OrderId;
811
+ const order = Order.create(orderId, "customer-456");
812
+
813
+ const item: OrderItem = {
814
+ id: "item-1" as ItemId,
815
+ productId: "prod-123",
816
+ quantity: 2,
817
+ price: 10.99,
818
+ };
819
+
820
+ order.addItem(item);
821
+ const foundItem = order.getItem(item.id);
822
+ console.log(sameEntity(item, foundItem!)); // true
823
+ ```
824
+
825
+ ### Using Result Type for Error Handling
826
+
827
+ ```typescript
828
+ import { ok, err, isOk, isErr, type Result, guard } from "@shirudo/ddd-kit";
829
+
830
+ type UserId = string;
831
+
832
+ function validateUserId(id: string): Result<UserId, string> {
833
+ const validation = guard(id.length > 0, "User ID cannot be empty");
834
+ if (isErr(validation)) {
835
+ return err(validation.error);
836
+ }
837
+ return ok(id as UserId);
838
+ }
839
+
840
+ function createUser(id: string): Result<{ id: UserId; name: string }, string> {
841
+ const userIdResult = validateUserId(id);
842
+ if (isErr(userIdResult)) {
843
+ return err(userIdResult.error);
844
+ }
845
+
846
+ return ok({
847
+ id: userIdResult.value,
848
+ name: "John Doe",
849
+ });
850
+ }
851
+
852
+ // Usage with type guards (recommended)
853
+ const result = createUser("user-123");
854
+ if (isOk(result)) {
855
+ console.log("User created:", result.value); // TypeScript knows result is Ok
856
+ } else {
857
+ console.error("Error:", result.error); // TypeScript knows result is Err
858
+ }
859
+
860
+ // Usage with ok property (also works)
861
+ const result2 = createUser("user-123");
862
+ if (result2.ok) {
863
+ console.log("User created:", result2.value);
864
+ } else {
865
+ console.error("Error:", result2.error);
866
+ }
867
+ ```
868
+
869
+ ## API Documentation
870
+
871
+ This package is written in TypeScript and provides full type definitions. All types and functions are exported from the main entry point. You can explore the available APIs through your IDE's autocomplete or by examining the type definitions in `node_modules/@shirudo/ddd-kit/dist/index.d.ts`.
872
+
873
+ Key exports include:
874
+ - `vo()`, `voEquals()`, `voWithValidation()`, `voWithValidationUnsafe()` - Value Object utilities
875
+ - `AggregateRoot<TId>` - Marker interface for Aggregate Roots
876
+ - `AggregateBase<TState, TId>` - Base class for aggregates without Event Sourcing (implements `AggregateRoot<TId>`)
877
+ - `AggregateEventSourced<TState, TEvent, TId>` - Base class for Event-Sourced aggregates (extends `AggregateBase`, implements `AggregateRoot<TId>`)
878
+ - `AggregateConfig`, `AggregateEventSourcedConfig` - Configuration interfaces
879
+ - `AggregateSnapshot<TState>` - Snapshot interface for performance optimization
880
+ - `sameAggregate()` - Aggregate equality helper
881
+ - `Entity<TId>` - Optional interface for entities with identity
882
+ - `sameEntity()`, `findEntityById()`, `hasEntityId()`, `removeEntityById()` - Entity helpers
883
+ - `Command`, `CommandHandler<C, R>` - Command interface and handler type for CQRS
884
+ - `Query`, `QueryHandler<Q, R>` - Query interface and handler type for CQRS
885
+ - `CommandBus`, `ICommandBus` - Command bus for centralized command execution
886
+ - `QueryBus`, `IQueryBus` - Query bus for centralized query execution (with `execute()` returning Result and `executeUnsafe()` throwing exceptions)
887
+ - `withCommit()` - Helper for transactional command execution with events
888
+ - `DomainEvent<T, P>`, `EventMetadata` - Domain event interfaces
889
+ - `createDomainEvent()`, `createDomainEventWithMetadata()` - Event creation helpers
890
+ - `copyMetadata()`, `mergeMetadata()` - Metadata utilities
891
+ - `EventBus<Evt>`, `EventBusImpl<Evt>` - Event bus interface and implementation for pub/sub pattern
892
+ - `EventHandler<Evt>` - Event handler function type
893
+ - `EventBus.subscribe()` - Subscribe handlers to event types
894
+ - `EventBus.publish()` - Publish events to all subscribers
895
+ - `Result<T, E>`, `ok()`, `err()`, `isOk()`, `isErr()` - Result type and helpers
896
+ - `Id<Tag>` - Branded ID type
897
+ - `IRepository<TState, TEvent, TAgg, TId>` - Repository interface
898
+ - `ISpecification<T>` - Specification interface
899
+ - `UnitOfWork` - Unit of Work interface
900
+ - `guard()` - Guard/validation helper
901
+
902
+ ## TypeScript Support
903
+
904
+ This package is built with TypeScript and provides comprehensive type safety. All APIs are fully typed, leveraging TypeScript's type system to ensure correctness at compile time. The package requires TypeScript 5.9.2 or higher and takes advantage of advanced TypeScript features like branded types, conditional types, and mapped types to provide a type-safe DDD experience.
905
+
906
+ ## Contributing
907
+
908
+ Contributions are welcome! Please read our contributing guidelines in [CONTRIBUTING.md](./CONTRIBUTING.md) before submitting pull requests. For bug reports and feature requests, please use the [GitHub issue tracker](https://github.com/shi-rudo/ddd-kit-ts/issues).
40
909
 
41
910
  ## License
42
911
 
43
912
  This project is licensed under the MIT License.
913
+
914
+ ## Author
915
+
916
+ **Shirudo**
917
+
918
+ - GitHub: [@shi-rudo](https://github.com/shi-rudo)
919
+ - Package: [@shirudo/ddd-kit](https://www.npmjs.com/package/@shirudo/ddd-kit)
920
+ - Repository: [ddd-kit-ts](https://github.com/shi-rudo/ddd-kit-ts)