@rineex/ddd 1.1.0 → 1.2.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 ADDED
@@ -0,0 +1,1291 @@
1
+ # @rineex/ddd
2
+
3
+ > Domain-Driven Design (DDD) utilities and abstractions for building
4
+ > maintainable, scalable, and testable Node.js applications with clear
5
+ > separation of concerns.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@rineex/ddd)](https://www.npmjs.com/package/@rineex/ddd)
8
+ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9+-blue.svg)](https://www.typescriptlang.org/)
10
+
11
+ ## Table of Contents
12
+
13
+ - [Overview](#overview)
14
+ - [Philosophy](#philosophy)
15
+ - [Installation](#installation)
16
+ - [Quick Start](#quick-start)
17
+ - [Core Concepts](#core-concepts)
18
+ - [Value Objects](#value-objects)
19
+ - [Entities](#entities)
20
+ - [Aggregate Roots](#aggregate-roots)
21
+ - [Domain Events](#domain-events)
22
+ - [Application Services](#application-services)
23
+ - [API Reference](#api-reference)
24
+ - [Examples](#examples)
25
+ - [Best Practices](#best-practices)
26
+ - [Error Handling](#error-handling)
27
+ - [TypeScript Support](#typescript-support)
28
+ - [Contributing](#contributing)
29
+ - [License](#license)
30
+
31
+ ## Overview
32
+
33
+ `@rineex/ddd` is a lightweight TypeScript library that provides production-grade
34
+ abstractions for implementing Domain-Driven Design patterns. It enforces
35
+ architectural constraints that prevent common pitfalls in large-scale
36
+ applications while maintaining flexibility for domain-specific requirements.
37
+
38
+ ### Key Features
39
+
40
+ - **Type-Safe Abstractions**: Fully typed base classes for all DDD building
41
+ blocks
42
+ - **Immutability by Default**: Value objects and entities are frozen to prevent
43
+ accidental mutations
44
+ - **Domain Events Support**: First-class support for event sourcing and
45
+ event-driven architectures
46
+ - **Validation Framework**: Built-in validation for value objects and entities
47
+ - **Zero Dependencies**: Only peer dependencies, minimal bundle footprint
48
+ - **Production Ready**: Used in high-performance systems at scale
49
+ - **Comprehensive Error Types**: Specific error classes for domain-driven
50
+ validation failures
51
+
52
+ ## Philosophy
53
+
54
+ This library is built on core principles that enable teams to:
55
+
56
+ 1. **Express Domain Logic Explicitly**: Make business rules clear and testable
57
+ 2. **Enforce Invariants**: Validate state transitions at the boundary
58
+ 3. **Manage Complexity**: Use aggregates to create transaction boundaries
59
+ 4. **Enable Event-Driven Architectures**: Capture and publish domain events
60
+ 5. **Maintain Testability**: Pure domain logic with no hidden dependencies
61
+
62
+ ## Installation
63
+
64
+ ```bash
65
+ npm install @rineex/ddd
66
+ # or
67
+ pnpm add @rineex/ddd
68
+ # or
69
+ yarn add @rineex/ddd
70
+ ```
71
+
72
+ ### Requirements
73
+
74
+ - **Node.js**: 18.0 or higher
75
+ - **TypeScript**: 5.0 or higher (recommended: 5.9+)
76
+ - **ES2020+**: Target the module to ES2020 or higher for optimal compatibility
77
+
78
+ ## Quick Start
79
+
80
+ Here's a minimal example to get started:
81
+
82
+ ```typescript
83
+ import {
84
+ Entity,
85
+ AggregateRoot,
86
+ ValueObject,
87
+ DomainEvent,
88
+ ApplicationServicePort,
89
+ AggregateId,
90
+ } from '@rineex/ddd';
91
+
92
+ // Define a Value Object
93
+ class Email extends ValueObject<string> {
94
+ public static create(value: string) {
95
+ return new Email(value);
96
+ }
97
+
98
+ protected validate(props: string): void {
99
+ if (!props.includes('@')) {
100
+ throw new Error('Invalid email');
101
+ }
102
+ }
103
+ }
104
+
105
+ // Define an Aggregate Root
106
+ interface UserProps {
107
+ email: Email;
108
+ isActive: boolean;
109
+ }
110
+
111
+ class User extends AggregateRoot<UserProps> {
112
+ get email(): Email {
113
+ return this.props.email;
114
+ }
115
+
116
+ get isActive(): boolean {
117
+ return this.props.isActive;
118
+ }
119
+
120
+ protected validate(): void {
121
+ if (!this.email) {
122
+ throw new Error('Email is required');
123
+ }
124
+ }
125
+ }
126
+
127
+ // Create and use
128
+ const userId = AggregateId.create();
129
+ const user = new User({
130
+ id: userId,
131
+ createdAt: new Date(),
132
+ props: { email: Email.create('user@example.com'), isActive: true },
133
+ });
134
+
135
+ user.addEvent(
136
+ new UserCreatedEvent({
137
+ id: 'evt-1',
138
+ aggregateId: userId.uuid,
139
+ schemaVersion: 1,
140
+ occurredAt: Date.now(),
141
+ payload: { email: user.email.value },
142
+ }),
143
+ );
144
+
145
+ const events = user.pullDomainEvents();
146
+ console.log(events); // [UserCreatedEvent]
147
+ ```
148
+
149
+ ## Core Concepts
150
+
151
+ ### Value Objects
152
+
153
+ Value Objects are immutable objects that are distinguished by their value rather
154
+ than their identity. They represent concepts within the domain that have no
155
+ lifecycle.
156
+
157
+ #### Characteristics
158
+
159
+ - **Immutable**: Cannot be changed after creation
160
+ - **Identity by Value**: Two value objects with the same properties are equal
161
+ - **Self-Validating**: Validation occurs during construction
162
+ - **No Side Effects**: Pure transformations only
163
+
164
+ #### Implementation
165
+
166
+ ```typescript
167
+ import { ValueObject } from '@rineex/ddd';
168
+
169
+ interface AddressProps {
170
+ street: string;
171
+ city: string;
172
+ postalCode: string;
173
+ country: string;
174
+ }
175
+
176
+ class Address extends ValueObject<AddressProps> {
177
+ get street(): string {
178
+ return this.props.street;
179
+ }
180
+
181
+ get city(): string {
182
+ return this.props.city;
183
+ }
184
+
185
+ get postalCode(): string {
186
+ return this.props.postalCode;
187
+ }
188
+
189
+ get country(): string {
190
+ return this.props.country;
191
+ }
192
+
193
+ public static create(props: AddressProps): Address {
194
+ return new Address(props);
195
+ }
196
+
197
+ protected validate(props: AddressProps): void {
198
+ if (!props.street || props.street.trim().length === 0) {
199
+ throw new Error('Street is required');
200
+ }
201
+ if (!props.city || props.city.trim().length === 0) {
202
+ throw new Error('City is required');
203
+ }
204
+ if (props.postalCode.length < 3) {
205
+ throw new Error('Invalid postal code');
206
+ }
207
+ }
208
+ }
209
+
210
+ // Usage
211
+ const address = Address.create({
212
+ street: '123 Main St',
213
+ city: 'New York',
214
+ postalCode: '10001',
215
+ country: 'USA',
216
+ });
217
+
218
+ // Immutability guaranteed
219
+ // address.props.street = 'foo'; // Error: Cannot assign to read only property
220
+ ```
221
+
222
+ #### Type Safety with `unwrapValueObject`
223
+
224
+ When working with collections of value objects, use the `unwrapValueObject`
225
+ utility:
226
+
227
+ ```typescript
228
+ import { unwrapValueObject, UnwrapValueObject } from '@rineex/ddd';
229
+
230
+ interface UserProps {
231
+ tags: Tag[]; // where Tag extends ValueObject<string>
232
+ }
233
+
234
+ const unwrapped: UnwrapValueObject<UserProps> = unwrapValueObject(userProps);
235
+ // { tags: ['admin', 'moderator'] }
236
+ ```
237
+
238
+ ### Entities
239
+
240
+ Entities are objects with a unique identity that persists over time. Unlike
241
+ value objects, they can be mutable and have a lifecycle.
242
+
243
+ #### Characteristics
244
+
245
+ - **Unique Identity**: Distinguished by a unique identifier (not just value)
246
+ - **Lifecycle**: Can be created, modified, and deleted
247
+ - **Mutable**: State can change, but identity remains constant
248
+ - **Equality by Identity**: Two entities with different properties but the same
249
+ ID are equal
250
+
251
+ #### Implementation
252
+
253
+ ```typescript
254
+ import { Entity, AggregateId } from '@rineex/ddd';
255
+ import type { CreateEntityProps } from '@rineex/ddd';
256
+
257
+ interface OrderItemProps {
258
+ productId: string;
259
+ quantity: number;
260
+ unitPrice: number;
261
+ }
262
+
263
+ class OrderItem extends Entity<OrderItemProps> {
264
+ get productId(): string {
265
+ return this.props.productId;
266
+ }
267
+
268
+ get quantity(): number {
269
+ return this.props.quantity;
270
+ }
271
+
272
+ get unitPrice(): number {
273
+ return this.props.unitPrice;
274
+ }
275
+
276
+ get total(): number {
277
+ return this.quantity * this.unitPrice;
278
+ }
279
+
280
+ protected validate(): void {
281
+ if (this.quantity <= 0) {
282
+ throw new Error('Quantity must be greater than zero');
283
+ }
284
+ if (this.unitPrice < 0) {
285
+ throw new Error('Unit price cannot be negative');
286
+ }
287
+ }
288
+ }
289
+
290
+ // Creating an entity
291
+ const item = new OrderItem({
292
+ id: AggregateId.create(),
293
+ createdAt: new Date(),
294
+ props: {
295
+ productId: 'prod-123',
296
+ quantity: 2,
297
+ unitPrice: 29.99,
298
+ },
299
+ });
300
+
301
+ console.log(item.total); // 59.98
302
+ ```
303
+
304
+ ### Aggregate Roots
305
+
306
+ Aggregate Roots are entities that serve as entry points to aggregates. They
307
+ enforce invariants, manage transactions, and raise domain events.
308
+
309
+ #### Characteristics
310
+
311
+ - **Boundary**: Define the scope of consistency within a transaction
312
+ - **Invariant Enforcement**: Validate rules that involve multiple entities or
313
+ value objects
314
+ - **Event Publisher**: Raise domain events to notify other parts of the system
315
+ - **Transaction Consistency**: All changes within an aggregate should be
316
+ persisted atomically
317
+
318
+ #### Implementation
319
+
320
+ ```typescript
321
+ import {
322
+ AggregateRoot,
323
+ AggregateId,
324
+ DomainEvent,
325
+ type DomainEventPayload,
326
+ } from '@rineex/ddd';
327
+
328
+ // Define domain events
329
+ class UserCreatedEvent extends DomainEvent {
330
+ public readonly eventName = 'UserCreated';
331
+
332
+ constructor(
333
+ props: Parameters<DomainEvent['constructor']>[0] & {
334
+ payload: { email: string };
335
+ },
336
+ ) {
337
+ super(props);
338
+ }
339
+ }
340
+
341
+ class UserEmailChangedEvent extends DomainEvent {
342
+ public readonly eventName = 'UserEmailChanged';
343
+ }
344
+
345
+ // Define the aggregate
346
+ interface UserProps {
347
+ email: string;
348
+ isActive: boolean;
349
+ }
350
+
351
+ class User extends AggregateRoot<UserProps> {
352
+ get email(): string {
353
+ return this.props.email;
354
+ }
355
+
356
+ get isActive(): boolean {
357
+ return this.props.isActive;
358
+ }
359
+
360
+ public static create(email: string, id?: AggregateId): User {
361
+ const user = new User({
362
+ id: id || AggregateId.create(),
363
+ createdAt: new Date(),
364
+ props: { email, isActive: true },
365
+ });
366
+
367
+ user.addEvent(
368
+ new UserCreatedEvent({
369
+ id: crypto.randomUUID(),
370
+ aggregateId: user.id.uuid,
371
+ schemaVersion: 1,
372
+ occurredAt: Date.now(),
373
+ payload: { email },
374
+ }),
375
+ );
376
+
377
+ return user;
378
+ }
379
+
380
+ public changeEmail(newEmail: string): void {
381
+ // Validate before changing
382
+ if (!newEmail.includes('@')) {
383
+ throw new Error('Invalid email format');
384
+ }
385
+
386
+ this.props = { ...this.props, email: newEmail };
387
+
388
+ this.addEvent(
389
+ new UserEmailChangedEvent({
390
+ id: crypto.randomUUID(),
391
+ aggregateId: this.id.uuid,
392
+ schemaVersion: 1,
393
+ occurredAt: Date.now(),
394
+ payload: { oldEmail: this.props.email, newEmail },
395
+ }),
396
+ );
397
+ }
398
+
399
+ protected validate(): void {
400
+ if (!this.props.email || !this.props.email.includes('@')) {
401
+ throw new Error('User must have a valid email');
402
+ }
403
+ }
404
+ }
405
+
406
+ // Usage
407
+ const user = User.create('john@example.com');
408
+ user.changeEmail('jane@example.com');
409
+
410
+ const events = user.pullDomainEvents(); // Remove events for publishing
411
+ console.log(events); // [UserCreatedEvent, UserEmailChangedEvent]
412
+ ```
413
+
414
+ #### Key Methods
415
+
416
+ - **`addEvent(event: DomainEvent): void`** - Adds a domain event after
417
+ validating invariants
418
+ - **`pullDomainEvents(): readonly DomainEvent[]`** - Retrieves and clears all
419
+ domain events
420
+ - **`validate(): void`** - Abstract method for enforcing aggregate invariants
421
+
422
+ ### Domain Events
423
+
424
+ Domain Events represent significant things that happened in the domain. They are
425
+ immutable records of past events and enable event-driven architectures.
426
+
427
+ #### Characteristics
428
+
429
+ - **Immutable**: Represent facts that have already occurred
430
+ - **Self-Describing**: Include all necessary information in the payload
431
+ - **Serializable**: Can be persisted and transmitted
432
+ - **Versioned**: Schema version allows for evolution
433
+ - **Timestamped**: Record when the event occurred
434
+
435
+ #### Implementation
436
+
437
+ ```typescript
438
+ import { DomainEvent, type DomainEventPayload } from '@rineex/ddd';
439
+
440
+ // Define event payloads (only primitives allowed)
441
+ interface OrderPlacedPayload extends DomainEventPayload {
442
+ customerId: string;
443
+ orderId: string;
444
+ totalAmount: number;
445
+ itemCount: number;
446
+ }
447
+
448
+ // Create event class
449
+ class OrderPlacedEvent extends DomainEvent<OrderPlacedPayload> {
450
+ public readonly eventName = 'OrderPlaced';
451
+
452
+ constructor(
453
+ props: Omit<
454
+ Parameters<DomainEvent<OrderPlacedPayload>['constructor']>[0],
455
+ 'payload'
456
+ > & {
457
+ payload: OrderPlacedPayload;
458
+ },
459
+ ) {
460
+ super(props);
461
+ }
462
+ }
463
+
464
+ // Using events
465
+ const event = new OrderPlacedEvent({
466
+ id: crypto.randomUUID(),
467
+ aggregateId: 'order-123',
468
+ schemaVersion: 1,
469
+ occurredAt: Date.now(),
470
+ payload: {
471
+ customerId: 'cust-456',
472
+ orderId: 'order-123',
473
+ totalAmount: 99.99,
474
+ itemCount: 3,
475
+ },
476
+ });
477
+
478
+ // Events are serializable
479
+ const primitives = event.toPrimitives();
480
+ // {
481
+ // id: '...',
482
+ // aggregateId: 'order-123',
483
+ // schemaVersion: 1,
484
+ // occurredAt: 1234567890,
485
+ // eventName: 'OrderPlaced',
486
+ // payload: { customerId: '...', orderId: '...', ... }
487
+ // }
488
+ ```
489
+
490
+ ### Application Services
491
+
492
+ Application Services orchestrate the business logic of the domain. They are the
493
+ entry points for handling use cases and commands.
494
+
495
+ #### Characteristics
496
+
497
+ - **Use Case Implementation**: Each service handles a single, well-defined use
498
+ case
499
+ - **Port Interface**: Implement a standard interface for consistency
500
+ - **Orchestration**: Coordinate domain objects, repositories, and external
501
+ services
502
+ - **Transaction Management**: Define transaction boundaries
503
+ - **Error Handling**: Map domain errors to application-level responses
504
+
505
+ #### Implementation
506
+
507
+ ```typescript
508
+ import type { ApplicationServicePort } from '@rineex/ddd';
509
+
510
+ // Define input and output DTOs
511
+ interface CreateUserInput {
512
+ email: string;
513
+ name: string;
514
+ }
515
+
516
+ interface CreateUserOutput {
517
+ id: string;
518
+ email: string;
519
+ name: string;
520
+ createdAt: string;
521
+ }
522
+
523
+ // Implement the service
524
+ class CreateUserService implements ApplicationServicePort<
525
+ CreateUserInput,
526
+ CreateUserOutput
527
+ > {
528
+ constructor(
529
+ private readonly userRepository: UserRepository,
530
+ private readonly eventPublisher: EventPublisher,
531
+ ) {}
532
+
533
+ async execute(args: CreateUserInput): Promise<CreateUserOutput> {
534
+ // Check for existing user
535
+ const existing = await this.userRepository.findByEmail(args.email);
536
+ if (existing) {
537
+ throw new Error(`User with email ${args.email} already exists`);
538
+ }
539
+
540
+ // Create aggregate
541
+ const user = User.create(args.email, args.name);
542
+
543
+ // Persist
544
+ await this.userRepository.save(user);
545
+
546
+ // Publish events
547
+ const events = user.pullDomainEvents();
548
+ await this.eventPublisher.publishAll(events);
549
+
550
+ return {
551
+ id: user.id.uuid,
552
+ email: user.email,
553
+ name: user.name,
554
+ createdAt: user.createdAt.toISOString(),
555
+ };
556
+ }
557
+ }
558
+
559
+ // Using the service
560
+ const createUserService = new CreateUserService(userRepository, eventPublisher);
561
+ const result = await createUserService.execute({
562
+ email: 'user@example.com',
563
+ name: 'John Doe',
564
+ });
565
+ ```
566
+
567
+ ## API Reference
568
+
569
+ ### Value Objects
570
+
571
+ #### `ValueObject<T>`
572
+
573
+ Abstract base class for all value objects.
574
+
575
+ ```typescript
576
+ export abstract class ValueObject<T> {
577
+ get value(): T;
578
+ public static is(vo: unknown): vo is ValueObject<unknown>;
579
+ public equals(other?: ValueObject<T>): boolean;
580
+ protected abstract validate(props: T): void;
581
+ }
582
+ ```
583
+
584
+ **Methods:**
585
+
586
+ - `value` - Returns the immutable properties
587
+ - `is(vo)` - Type guard for runtime checking
588
+ - `equals(other)` - Deep equality comparison
589
+ - `validate(props)` - Validation logic (must be implemented)
590
+
591
+ ### Entities
592
+
593
+ #### `Entity<EntityProps>`
594
+
595
+ Abstract base class for domain entities.
596
+
597
+ ```typescript
598
+ export abstract class Entity<EntityProps> {
599
+ readonly id: AggregateId;
600
+ readonly createdAt: Date;
601
+ readonly metadata: { createdAt: string; id: string };
602
+ abstract validate(): void;
603
+ equals(entity: unknown): boolean;
604
+ }
605
+ ```
606
+
607
+ **Constructor:**
608
+
609
+ ```typescript
610
+ new Entity({
611
+ id?: AggregateId; // Generated if not provided
612
+ createdAt: Date; // Required
613
+ props: EntityProps; // Domain-specific properties
614
+ })
615
+ ```
616
+
617
+ ### Aggregate Roots
618
+
619
+ #### `AggregateRoot<EntityProps>`
620
+
621
+ Extends `Entity` with domain event support.
622
+
623
+ ```typescript
624
+ export abstract class AggregateRoot<EntityProps> extends Entity<EntityProps> {
625
+ readonly domainEvents: readonly DomainEvent[];
626
+ abstract validate(): void;
627
+ addEvent(event: DomainEvent): void;
628
+ pullDomainEvents(): readonly DomainEvent[];
629
+ }
630
+ ```
631
+
632
+ **Methods:**
633
+
634
+ - `addEvent(event)` - Add an event after validating invariants
635
+ - `pullDomainEvents()` - Get and clear all recorded events
636
+ - `domainEvents` - Read-only view of current events
637
+
638
+ ### Domain Events
639
+
640
+ #### `DomainEvent<T extends DomainEventPayload>`
641
+
642
+ Abstract base class for domain events.
643
+
644
+ ```typescript
645
+ export abstract class DomainEvent<T extends DomainEventPayload> {
646
+ abstract readonly eventName: string;
647
+ readonly id: string;
648
+ readonly aggregateId: string;
649
+ readonly schemaVersion: number;
650
+ readonly occurredAt: number;
651
+ readonly payload: Readonly<T>;
652
+
653
+ toPrimitives(): {
654
+ id: string;
655
+ aggregateId: string;
656
+ schemaVersion: number;
657
+ occurredAt: number;
658
+ eventName: string;
659
+ payload: T;
660
+ };
661
+ }
662
+ ```
663
+
664
+ ### Application Services
665
+
666
+ #### `ApplicationServicePort<I, O>`
667
+
668
+ Interface for application services.
669
+
670
+ ```typescript
671
+ export interface ApplicationServicePort<I, O> {
672
+ execute: (args: I) => Promise<O>;
673
+ }
674
+ ```
675
+
676
+ ### Value Objects (Pre-built)
677
+
678
+ #### `AggregateId`
679
+
680
+ Represents the unique identifier for an aggregate.
681
+
682
+ ```typescript
683
+ class AggregateId extends ValueObject<{ uuid: string }> {
684
+ get uuid(): string;
685
+ public static create(id?: string): AggregateId;
686
+ }
687
+ ```
688
+
689
+ #### `IPAddress`
690
+
691
+ Validates IPv4 and IPv6 addresses.
692
+
693
+ ```typescript
694
+ class IPAddress extends ValueObject<string> {
695
+ public static create(value: string): IPAddress;
696
+ }
697
+ ```
698
+
699
+ #### `URL`
700
+
701
+ Validates web URLs.
702
+
703
+ ```typescript
704
+ class URL extends ValueObject<string> {
705
+ public static create(value: string): URL;
706
+ }
707
+ ```
708
+
709
+ #### `UserAgent`
710
+
711
+ Parses and validates user agent strings.
712
+
713
+ ```typescript
714
+ class UserAgent extends ValueObject<string> {
715
+ public static create(value: string): UserAgent;
716
+ }
717
+ ```
718
+
719
+ ### Error Types
720
+
721
+ #### `DomainError`
722
+
723
+ Base class for all domain errors.
724
+
725
+ ```typescript
726
+ export class DomainError extends Error {
727
+ constructor(message: string);
728
+ }
729
+ ```
730
+
731
+ #### `EntityValidationError`
732
+
733
+ Thrown when entity validation fails.
734
+
735
+ ```typescript
736
+ export class EntityValidationError extends DomainError {}
737
+ ```
738
+
739
+ #### `InvalidValueObjectError`
740
+
741
+ Thrown when value object validation fails.
742
+
743
+ ```typescript
744
+ export class InvalidValueObjectError extends DomainError {}
745
+ ```
746
+
747
+ #### `ApplicationError`
748
+
749
+ Thrown for application-level errors.
750
+
751
+ ```typescript
752
+ export class ApplicationError extends DomainError {}
753
+ ```
754
+
755
+ ## Examples
756
+
757
+ ### Complete Order Management System
758
+
759
+ Here's a realistic example showing how to structure a domain with multiple
760
+ aggregates:
761
+
762
+ ```typescript
763
+ import {
764
+ AggregateRoot,
765
+ AggregateId,
766
+ ValueObject,
767
+ DomainEvent,
768
+ Entity,
769
+ ApplicationServicePort,
770
+ } from '@rineex/ddd';
771
+
772
+ // ============ Value Objects ============
773
+
774
+ interface MoneyProps {
775
+ amount: number;
776
+ currency: string;
777
+ }
778
+
779
+ class Money extends ValueObject<MoneyProps> {
780
+ get amount(): number {
781
+ return this.props.amount;
782
+ }
783
+
784
+ get currency(): string {
785
+ return this.props.currency;
786
+ }
787
+
788
+ public static create(amount: number, currency = 'USD'): Money {
789
+ return new Money({ amount, currency });
790
+ }
791
+
792
+ protected validate(props: MoneyProps): void {
793
+ if (props.amount < 0) {
794
+ throw new Error('Amount cannot be negative');
795
+ }
796
+ if (!props.currency || props.currency.length !== 3) {
797
+ throw new Error('Invalid currency code');
798
+ }
799
+ }
800
+ }
801
+
802
+ // ============ Entities ============
803
+
804
+ interface OrderLineProps {
805
+ productId: string;
806
+ quantity: number;
807
+ price: Money;
808
+ }
809
+
810
+ class OrderLine extends Entity<OrderLineProps> {
811
+ get productId(): string {
812
+ return this.props.productId;
813
+ }
814
+
815
+ get quantity(): number {
816
+ return this.props.quantity;
817
+ }
818
+
819
+ get price(): Money {
820
+ return this.props.price;
821
+ }
822
+
823
+ get subtotal(): Money {
824
+ return Money.create(this.price.amount * this.quantity, this.price.currency);
825
+ }
826
+
827
+ protected validate(): void {
828
+ if (this.quantity <= 0) {
829
+ throw new Error('Quantity must be positive');
830
+ }
831
+ }
832
+ }
833
+
834
+ // ============ Domain Events ============
835
+
836
+ class OrderCreatedEvent extends DomainEvent {
837
+ public readonly eventName = 'OrderCreated';
838
+ }
839
+
840
+ class OrderLineAddedEvent extends DomainEvent {
841
+ public readonly eventName = 'OrderLineAdded';
842
+ }
843
+
844
+ class OrderCompletedEvent extends DomainEvent {
845
+ public readonly eventName = 'OrderCompleted';
846
+ }
847
+
848
+ // ============ Aggregate Root ============
849
+
850
+ interface OrderProps {
851
+ customerId: string;
852
+ lines: OrderLine[];
853
+ status: 'pending' | 'completed' | 'cancelled';
854
+ total: Money;
855
+ }
856
+
857
+ class Order extends AggregateRoot<OrderProps> {
858
+ get customerId(): string {
859
+ return this.props.customerId;
860
+ }
861
+
862
+ get lines(): OrderLine[] {
863
+ return this.props.lines;
864
+ }
865
+
866
+ get status(): string {
867
+ return this.props.status;
868
+ }
869
+
870
+ get total(): Money {
871
+ return this.props.total;
872
+ }
873
+
874
+ public static create(customerId: string, id?: AggregateId): Order {
875
+ const order = new Order({
876
+ id: id || AggregateId.create(),
877
+ createdAt: new Date(),
878
+ props: {
879
+ customerId,
880
+ lines: [],
881
+ status: 'pending',
882
+ total: Money.create(0),
883
+ },
884
+ });
885
+
886
+ order.addEvent(
887
+ new OrderCreatedEvent({
888
+ id: crypto.randomUUID(),
889
+ aggregateId: order.id.uuid,
890
+ schemaVersion: 1,
891
+ occurredAt: Date.now(),
892
+ payload: { customerId },
893
+ }),
894
+ );
895
+
896
+ return order;
897
+ }
898
+
899
+ public addLine(productId: string, quantity: number, price: Money): void {
900
+ const line = new OrderLine({
901
+ id: AggregateId.create(),
902
+ createdAt: new Date(),
903
+ props: { productId, quantity, price },
904
+ });
905
+
906
+ this.props.lines.push(line);
907
+ this.recalculateTotal();
908
+
909
+ this.addEvent(
910
+ new OrderLineAddedEvent({
911
+ id: crypto.randomUUID(),
912
+ aggregateId: this.id.uuid,
913
+ schemaVersion: 1,
914
+ occurredAt: Date.now(),
915
+ payload: { productId, quantity },
916
+ }),
917
+ );
918
+ }
919
+
920
+ public complete(): void {
921
+ if (this.status !== 'pending') {
922
+ throw new Error('Only pending orders can be completed');
923
+ }
924
+
925
+ this.props.status = 'completed';
926
+
927
+ this.addEvent(
928
+ new OrderCompletedEvent({
929
+ id: crypto.randomUUID(),
930
+ aggregateId: this.id.uuid,
931
+ schemaVersion: 1,
932
+ occurredAt: Date.now(),
933
+ payload: { total: this.total.amount },
934
+ }),
935
+ );
936
+ }
937
+
938
+ private recalculateTotal(): void {
939
+ const sum = this.lines.reduce((acc, line) => acc + line.subtotal.amount, 0);
940
+ this.props.total = Money.create(sum, 'USD');
941
+ }
942
+
943
+ protected validate(): void {
944
+ if (!this.customerId) {
945
+ throw new Error('Customer ID is required');
946
+ }
947
+ if (this.lines.length === 0) {
948
+ throw new Error('Order must have at least one line');
949
+ }
950
+ }
951
+ }
952
+
953
+ // ============ Application Service ============
954
+
955
+ interface CreateOrderInput {
956
+ customerId: string;
957
+ lines: { productId: string; quantity: number; price: number }[];
958
+ }
959
+
960
+ interface CreateOrderOutput {
961
+ id: string;
962
+ customerId: string;
963
+ total: number;
964
+ lineCount: number;
965
+ }
966
+
967
+ class CreateOrderService implements ApplicationServicePort<
968
+ CreateOrderInput,
969
+ CreateOrderOutput
970
+ > {
971
+ constructor(private readonly orderRepository: OrderRepository) {}
972
+
973
+ async execute(args: CreateOrderInput): Promise<CreateOrderOutput> {
974
+ const order = Order.create(args.customerId);
975
+
976
+ for (const line of args.lines) {
977
+ order.addLine(line.productId, line.quantity, Money.create(line.price));
978
+ }
979
+
980
+ order.complete();
981
+ await this.orderRepository.save(order);
982
+
983
+ return {
984
+ id: order.id.uuid,
985
+ customerId: order.customerId,
986
+ total: order.total.amount,
987
+ lineCount: order.lines.length,
988
+ };
989
+ }
990
+ }
991
+ ```
992
+
993
+ ## Best Practices
994
+
995
+ ### 1. **Make Invalid States Impossible**
996
+
997
+ Use type system and validation to make invalid states impossible to construct:
998
+
999
+ ```typescript
1000
+ // ❌ BAD: Can create invalid state
1001
+ class User {
1002
+ email: string;
1003
+ isVerified: boolean;
1004
+ }
1005
+
1006
+ // ✅ GOOD: Invalid state impossible
1007
+ class UnverifiedUser extends ValueObject<{ email: string }> {}
1008
+ class VerifiedUser extends ValueObject<{ email: string; verifiedAt: Date }> {}
1009
+ ```
1010
+
1011
+ ### 2. **Keep Aggregates Small**
1012
+
1013
+ Prefer small aggregates with clear boundaries over large aggregates with many
1014
+ entities:
1015
+
1016
+ ```typescript
1017
+ // ❌ BAD: Too many entities in one aggregate
1018
+ class Store extends AggregateRoot {
1019
+ employees: Employee[];
1020
+ inventory: InventoryItem[];
1021
+ orders: Order[];
1022
+ // ... many more
1023
+ }
1024
+
1025
+ // ✅ GOOD: Separate aggregates with references
1026
+ class Store extends AggregateRoot {
1027
+ name: string;
1028
+ // Reference to other aggregates by ID only
1029
+ employeeIds: AggregateId[];
1030
+ }
1031
+
1032
+ class Inventory extends AggregateRoot {
1033
+ storeId: AggregateId;
1034
+ items: InventoryItem[];
1035
+ }
1036
+ ```
1037
+
1038
+ ### 3. **Use Value Objects for Primitive Types**
1039
+
1040
+ Wrap primitives that have domain meaning:
1041
+
1042
+ ```typescript
1043
+ // ❌ BAD: Raw primitive types
1044
+ interface User {
1045
+ email: string;
1046
+ phone: string;
1047
+ age: number;
1048
+ }
1049
+
1050
+ // ✅ GOOD: Domain-meaningful value objects
1051
+ interface User {
1052
+ email: Email;
1053
+ phone: PhoneNumber;
1054
+ age: Age;
1055
+ }
1056
+ ```
1057
+
1058
+ ### 4. **Validate at Boundaries**
1059
+
1060
+ Perform all validation when creating aggregates, not repeatedly:
1061
+
1062
+ ```typescript
1063
+ // ❌ BAD: Repeated validation
1064
+ function updateEmail(email: string) {
1065
+ if (!isValidEmail(email)) throw Error();
1066
+ }
1067
+
1068
+ function sendEmail(email: string) {
1069
+ if (!isValidEmail(email)) throw Error();
1070
+ }
1071
+
1072
+ // ✅ GOOD: Single validation point
1073
+ const email = Email.create(value); // Throws if invalid
1074
+ updateEmail(email);
1075
+ sendEmail(email);
1076
+ ```
1077
+
1078
+ ### 5. **Event-Driven State Changes**
1079
+
1080
+ All changes should be reflected in domain events:
1081
+
1082
+ ```typescript
1083
+ // ✅ GOOD: Changes recorded as events
1084
+ class User extends AggregateRoot {
1085
+ changeEmail(newEmail: Email): void {
1086
+ const oldEmail = this.email;
1087
+ this.props.email = newEmail;
1088
+
1089
+ this.addEvent(
1090
+ new EmailChangedEvent({
1091
+ id: crypto.randomUUID(),
1092
+ aggregateId: this.id.uuid,
1093
+ schemaVersion: 1,
1094
+ occurredAt: Date.now(),
1095
+ payload: { oldEmail: oldEmail.value, newEmail: newEmail.value },
1096
+ }),
1097
+ );
1098
+ }
1099
+ }
1100
+ ```
1101
+
1102
+ ### 6. **Publish Events After Persistence**
1103
+
1104
+ Always publish events after persisting the aggregate:
1105
+
1106
+ ```typescript
1107
+ async function handle(command: CreateUserCommand): Promise<void> {
1108
+ // Create aggregate
1109
+ const user = User.create(command.email);
1110
+
1111
+ // Persist first
1112
+ await userRepository.save(user);
1113
+
1114
+ // Then publish
1115
+ const events = user.pullDomainEvents();
1116
+ await eventPublisher.publishAll(events);
1117
+ }
1118
+ ```
1119
+
1120
+ ### 7. **Immutability by Convention**
1121
+
1122
+ Even though TypeScript doesn't enforce it, treat all domain objects as
1123
+ immutable:
1124
+
1125
+ ```typescript
1126
+ // ✅ GOOD: Replace entire aggregate when state changes
1127
+ class User extends AggregateRoot {
1128
+ changeName(newName: string): void {
1129
+ // Don't mutate: this.props.name = newName;
1130
+
1131
+ // Instead, create new object:
1132
+ this.props = { ...this.props, name: newName };
1133
+ }
1134
+ }
1135
+ ```
1136
+
1137
+ ## Error Handling
1138
+
1139
+ Handle different error scenarios appropriately:
1140
+
1141
+ ```typescript
1142
+ import {
1143
+ DomainError,
1144
+ EntityValidationError,
1145
+ InvalidValueObjectError,
1146
+ ApplicationError,
1147
+ } from '@rineex/ddd';
1148
+
1149
+ try {
1150
+ const email = Email.create('invalid-email');
1151
+ } catch (error) {
1152
+ if (error instanceof InvalidValueObjectError) {
1153
+ // Handle value object validation errors
1154
+ console.error('Invalid email format:', error.message);
1155
+ }
1156
+ }
1157
+
1158
+ try {
1159
+ const user = new User({
1160
+ id: AggregateId.create(),
1161
+ createdAt: new Date(),
1162
+ props: { email: Email.create('user@example.com') },
1163
+ });
1164
+ user.validate();
1165
+ } catch (error) {
1166
+ if (error instanceof EntityValidationError) {
1167
+ // Handle entity validation errors
1168
+ console.error('User invariant violated:', error.message);
1169
+ }
1170
+ }
1171
+
1172
+ try {
1173
+ await userService.execute(input);
1174
+ } catch (error) {
1175
+ if (error instanceof ApplicationError) {
1176
+ // Handle application-level errors
1177
+ console.error('Service failed:', error.message);
1178
+ } else if (error instanceof DomainError) {
1179
+ // Catch-all for domain errors
1180
+ console.error('Domain error:', error.message);
1181
+ }
1182
+ }
1183
+ ```
1184
+
1185
+ ## TypeScript Support
1186
+
1187
+ This library is built with TypeScript 5.9+ and provides comprehensive type
1188
+ safety:
1189
+
1190
+ ```typescript
1191
+ // Full type inference
1192
+ const user = User.create('user@example.com');
1193
+ const id: AggregateId = user.id; // Correctly typed
1194
+
1195
+ // Type-safe event handling
1196
+ const events = user.pullDomainEvents();
1197
+ events.forEach(event => {
1198
+ if (event instanceof UserCreatedEvent) {
1199
+ // Type guard works correctly
1200
+ const payload = event.payload; // Correctly inferred type
1201
+ }
1202
+ });
1203
+
1204
+ // Proper generic constraints
1205
+ class MyAggregate extends AggregateRoot<MyProps> {
1206
+ // Full type safety with MyProps
1207
+ }
1208
+ ```
1209
+
1210
+ ### Recommended TypeScript Configuration
1211
+
1212
+ ```json
1213
+ {
1214
+ "compilerOptions": {
1215
+ "target": "ES2020",
1216
+ "module": "ESNext",
1217
+ "lib": ["ES2020"],
1218
+ "strict": true,
1219
+ "skipLibCheck": true,
1220
+ "forceConsistentCasingInFileNames": true,
1221
+ "resolveJsonModule": true,
1222
+ "esModuleInterop": true,
1223
+ "declaration": true,
1224
+ "declarationMap": true,
1225
+ "sourceMap": true
1226
+ }
1227
+ }
1228
+ ```
1229
+
1230
+ ## Contributing
1231
+
1232
+ Contributions are welcome! Please follow these guidelines:
1233
+
1234
+ 1. **Fork** the repository
1235
+ 2. **Create** a feature branch (`git checkout -b feature/amazing-feature`)
1236
+ 3. **Write** tests for new functionality
1237
+ 4. **Ensure** all tests pass (`pnpm test`)
1238
+ 5. **Follow** the code style (`pnpm lint`)
1239
+ 6. **Commit** with clear messages
1240
+ 7. **Push** to the branch and create a Pull Request
1241
+
1242
+ ### Development Setup
1243
+
1244
+ ```bash
1245
+ # Install dependencies
1246
+ pnpm install
1247
+
1248
+ # Run tests
1249
+ pnpm test
1250
+
1251
+ # Run linter
1252
+ pnpm lint
1253
+
1254
+ # Check types
1255
+ pnpm check-types
1256
+
1257
+ # Build the package
1258
+ pnpm build
1259
+ ```
1260
+
1261
+ ### Code Style
1262
+
1263
+ - Follow the existing code style
1264
+ - Use TypeScript strict mode
1265
+ - Write descriptive variable and function names
1266
+ - Add JSDoc comments for public APIs
1267
+ - Keep functions small and focused
1268
+
1269
+ ## License
1270
+
1271
+ This project is licensed under the Apache License 2.0 - see the
1272
+ [LICENSE](LICENSE) file for details.
1273
+
1274
+ ## Related Resources
1275
+
1276
+ - [Domain-Driven Design: Tackling Complexity in the Heart of Software](https://www.domainlanguage.com/ddd/)
1277
+ by Eric Evans
1278
+ - [Implementing Domain-Driven Design](https://vaughnvernon.com/books/) by Vaughn
1279
+ Vernon
1280
+ - [Architecture Patterns with Python](https://www.cosmicpython.com/) by Harry J.
1281
+ W. Percival and Bob Gregory
1282
+ - [TypeScript Handbook](https://www.typescriptlang.org/docs/)
1283
+
1284
+ ## Support
1285
+
1286
+ For issues, questions, or suggestions, please open an issue on
1287
+ [GitHub](https://github.com/rineex/core/issues).
1288
+
1289
+ ---
1290
+
1291
+ **Made with ❤️ by the Rineex Team**