@rineex/ddd 3.0.0 → 3.0.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.
Files changed (2) hide show
  1. package/README.md +487 -1498
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,8 +1,7 @@
1
1
  # @rineex/ddd
2
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.
3
+ > Domain-Driven Design (DDD) primitives for building maintainable, scalable
4
+ > TypeScript applications.
6
5
 
7
6
  [![npm version](https://img.shields.io/npm/v/@rineex/ddd)](https://www.npmjs.com/package/@rineex/ddd)
8
7
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
@@ -11,1751 +10,741 @@
11
10
  ## Table of Contents
12
11
 
13
12
  - [Overview](#overview)
14
- - [Philosophy](#philosophy)
15
13
  - [Installation](#installation)
16
- - [Quick Start](#quick-start)
17
- - [Core Concepts](#core-concepts)
14
+ - [Package Exports](#package-exports)
18
15
  - [Value Objects](#value-objects)
16
+ - [Primitive Value Objects](#primitive-value-objects)
19
17
  - [Entities](#entities)
20
18
  - [Aggregate Roots](#aggregate-roots)
21
19
  - [Domain Events](#domain-events)
20
+ - [Domain Errors](#domain-errors)
21
+ - [Result Type](#result-type)
22
22
  - [Application Services](#application-services)
23
+ - [Ports & Utilities](#ports--utilities)
24
+ - [Integration Guide](#integration-guide)
23
25
  - [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
26
  - [License](#license)
30
27
 
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
28
+ ---
39
29
 
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
30
+ ## Overview
51
31
 
52
- ## Philosophy
32
+ `@rineex/ddd` provides type-safe building blocks for implementing Domain-Driven
33
+ Design patterns. Used by `@rineex/authentication` and other Rineex packages.
53
34
 
54
- This library is built on core principles that enable teams to:
35
+ **Features:** Value Objects, Entities, Aggregate Roots, Domain Events, Domain
36
+ Errors (extensible namespaces), Result type, Application Service port, Clock
37
+ port, HTTP status constants.
55
38
 
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
39
+ ---
61
40
 
62
41
  ## Installation
63
42
 
64
43
  ```bash
65
- npm install @rineex/ddd
66
- # or
67
44
  pnpm add @rineex/ddd
68
- # or
69
- yarn add @rineex/ddd
70
45
  ```
71
46
 
72
- ### Requirements
47
+ **Requirements:** Node.js 18+, TypeScript 5.0+, ES2020+ target
73
48
 
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
49
+ ---
79
50
 
80
- Here's a minimal example to get started:
51
+ ## Package Exports
81
52
 
82
53
  ```typescript
83
54
  import {
55
+ ValueObject,
56
+ PrimitiveValueObject,
84
57
  Entity,
85
58
  AggregateRoot,
86
- ValueObject,
87
59
  DomainEvent,
88
- ApplicationServicePort,
89
60
  AggregateId,
90
- type EntityProps,
61
+ DomainID,
62
+ Email,
63
+ DomainError,
64
+ InvalidValueObjectError,
65
+ EntityValidationError,
66
+ InvalidValueError,
67
+ InvalidStateError,
68
+ InternalError,
69
+ TimeoutError,
70
+ ApplicationError,
71
+ ApplicationServicePort,
72
+ Result,
73
+ ClockPort,
74
+ EntityId,
75
+ EntityProps,
76
+ DomainEventPayload,
77
+ CreateEventProps,
78
+ UnixTimestampMillis,
79
+ HttpStatus,
80
+ HttpStatusMessage,
81
+ deepFreeze,
91
82
  } from '@rineex/ddd';
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Value Objects
88
+
89
+ Value objects are immutable and defined by attributes. Use `ValueObject<T>` for
90
+ composite structures. Props are deep-frozen in the constructor.
91
+
92
+ ### Example (from `vo.spec.ts`)
93
+
94
+ ```typescript
95
+ import { ValueObject, InvalidValueObjectError } from '@rineex/ddd';
92
96
 
93
- // Define a Value Object
94
- class Email extends ValueObject<string> {
95
- public static create(value: string) {
96
- return new Email(value);
97
+ class TestValueObject extends ValueObject<{ name: string; age: number }> {
98
+ constructor(props: { name: string; age: number }) {
99
+ super(props);
97
100
  }
98
101
 
99
- protected validate(props: string): void {
100
- if (!props.includes('@')) {
101
- throw new Error('Invalid email');
102
+ protected validate(props: { name: string; age: number }): void {
103
+ if (!props.name?.trim()) {
104
+ throw InvalidValueObjectError.create('Name is required');
105
+ }
106
+ if (props.age < 0 || props.age > 150) {
107
+ throw InvalidValueObjectError.create('Age must be between 0 and 150');
102
108
  }
103
109
  }
104
110
  }
105
111
 
106
- // Define an Aggregate Root
107
- interface UserProps {
108
- email: Email;
109
- isActive: boolean;
110
- }
112
+ // Usage
113
+ const vo = new TestValueObject({ name: 'John', age: 30 });
114
+ vo.value; // { name: 'John', age: 30 }
115
+ vo.equals(other); // deep equality
116
+ vo.toJSON(); // returns props
117
+ vo.toString(); // JSON.stringify(props)
118
+ ValueObject.is(vo); // type guard
119
+ ```
111
120
 
112
- class User extends AggregateRoot<AggregateId, UserProps> {
113
- get email(): Email {
114
- return this.props.email;
115
- }
121
+ ### Simple Value Object (wraps a single value)
116
122
 
117
- get isActive(): boolean {
118
- return this.props.isActive;
123
+ ```typescript
124
+ class SimpleValueObject extends ValueObject<string> {
125
+ constructor(value: string) {
126
+ super(value);
119
127
  }
120
128
 
121
- protected validate(): void {
122
- if (!this.email) {
123
- throw new Error('Email is required');
129
+ protected validate(value: string): void {
130
+ if (!value?.length) {
131
+ throw InvalidValueObjectError.create('Value cannot be empty');
124
132
  }
125
133
  }
126
-
127
- public toObject(): Record<string, unknown> {
128
- return {
129
- id: this.id.toString(),
130
- createdAt: this.createdAt.toISOString(),
131
- email: this.email.value,
132
- isActive: this.isActive,
133
- };
134
- }
135
- }
136
-
137
- // Define a Domain Event
138
- class UserCreatedEvent extends DomainEvent<AggregateId> {
139
- public readonly eventName = 'UserCreated';
140
134
  }
141
-
142
- // Create and use
143
- const userId = AggregateId.generate();
144
- const user = new User({
145
- id: userId,
146
- createdAt: new Date(),
147
- props: { email: Email.create('user@example.com'), isActive: true },
148
- });
149
-
150
- user.addEvent(
151
- new UserCreatedEvent({
152
- aggregateId: userId,
153
- schemaVersion: 1,
154
- occurredAt: Date.now(),
155
- payload: { email: user.email.value },
156
- }),
157
- );
158
-
159
- const events = user.pullDomainEvents();
160
- console.log(events); // [UserCreatedEvent]
161
135
  ```
162
136
 
163
- ## Core Concepts
164
-
165
- ### Value Objects
166
-
167
- Value Objects are immutable objects that are distinguished by their value rather
168
- than their identity. They represent concepts within the domain that have no
169
- lifecycle.
137
+ ---
170
138
 
171
- #### Characteristics
139
+ ## Primitive Value Objects
172
140
 
173
- - **Immutable**: Cannot be changed after creation
174
- - **Identity by Value**: Two value objects with the same properties are equal
175
- - **Self-Validating**: Validation occurs during construction
176
- - **No Side Effects**: Pure transformations only
141
+ For single primitives (string, number, boolean), extend
142
+ `PrimitiveValueObject<T>`. Equality is by reference (`===`).
177
143
 
178
- #### Implementation
144
+ ### Example (from `primitive-vo.spec.ts`)
179
145
 
180
146
  ```typescript
181
- import { ValueObject } from '@rineex/ddd';
182
-
183
- interface AddressProps {
184
- street: string;
185
- city: string;
186
- postalCode: string;
187
- country: string;
188
- }
147
+ import { PrimitiveValueObject, InvalidValueObjectError } from '@rineex/ddd';
189
148
 
190
- class Address extends ValueObject<AddressProps> {
191
- get street(): string {
192
- return this.props.street;
149
+ class StringVO extends PrimitiveValueObject<string> {
150
+ constructor(value: string) {
151
+ super(value);
193
152
  }
194
153
 
195
- get city(): string {
196
- return this.props.city;
197
- }
198
-
199
- get postalCode(): string {
200
- return this.props.postalCode;
201
- }
202
-
203
- get country(): string {
204
- return this.props.country;
154
+ protected validate(value: string): void {
155
+ if (!value?.length) {
156
+ throw InvalidValueObjectError.create('String cannot be empty');
157
+ }
205
158
  }
159
+ }
206
160
 
207
- public static create(props: AddressProps): Address {
208
- return new Address(props);
161
+ class NumberVO extends PrimitiveValueObject<number> {
162
+ constructor(value: number) {
163
+ super(value);
209
164
  }
210
165
 
211
- protected validate(props: AddressProps): void {
212
- if (!props.street || props.street.trim().length === 0) {
213
- throw new Error('Street is required');
214
- }
215
- if (!props.city || props.city.trim().length === 0) {
216
- throw new Error('City is required');
217
- }
218
- if (props.postalCode.length < 3) {
219
- throw new Error('Invalid postal code');
166
+ protected validate(value: number): void {
167
+ if (value < 0) {
168
+ throw InvalidValueObjectError.create('Number must be non-negative');
220
169
  }
221
170
  }
222
171
  }
223
172
 
224
173
  // Usage
225
- const address = Address.create({
226
- street: '123 Main St',
227
- city: 'New York',
228
- postalCode: '10001',
229
- country: 'USA',
230
- });
231
-
232
- // Immutability guaranteed
233
- // address.props.street = 'foo'; // Error: Cannot assign to read only property
174
+ const s = new StringVO('test');
175
+ s.value; // 'test'
176
+ s.getValue(); // deprecated, use .value
177
+ s.toString(); // 'test'
178
+ s.equals(new StringVO('test')); // true
234
179
  ```
235
180
 
236
- #### Primitive Value Objects
237
-
238
- For value objects that wrap a single primitive (string, number, or boolean), use
239
- `PrimitiveValueObject` for better performance:
181
+ ### Pre-built: Email
240
182
 
241
183
  ```typescript
242
- import { PrimitiveValueObject } from '@rineex/ddd';
243
-
244
- class Email extends PrimitiveValueObject<string> {
245
- public static create(value: string): Email {
246
- return new Email(value);
247
- }
248
-
249
- protected validate(value: string): void {
250
- if (!value.includes('@')) {
251
- throw new Error('Invalid email');
252
- }
253
- }
254
- }
184
+ import { Email } from '@rineex/ddd';
255
185
 
256
- // Usage
257
- const email = Email.create('user@example.com');
258
- console.log(email.getValue()); // 'user@example.com'
186
+ const email = Email.fromString('user@example.com');
187
+ // or: new Email('user@example.com')
188
+ email.value; // 'user@example.com'
189
+ email.toString();
259
190
  ```
260
191
 
261
- #### Type Safety with `unwrapValueObject`
262
-
263
- When working with collections of value objects, use the `unwrapValueObject`
264
- utility:
192
+ ### Pre-built: AggregateId & DomainID
265
193
 
266
194
  ```typescript
267
- import { unwrapValueObject, type UnwrapValueObject } from '@rineex/ddd';
195
+ import { AggregateId, DomainID } from '@rineex/ddd';
268
196
 
269
- interface UserProps {
270
- tags: Tag[]; // where Tag extends ValueObject<string>
271
- }
197
+ // AggregateId
198
+ const id = AggregateId.generate();
199
+ const fromStr = AggregateId.fromString('550e8400-e29b-41d4-a716-446655440000');
272
200
 
273
- const unwrapped: UnwrapValueObject<UserProps> = unwrapValueObject(userProps);
274
- // { tags: ['admin', 'moderator'] }
275
- ```
201
+ // DomainID extend for custom IDs
202
+ class AuthAttemptId extends DomainID {}
276
203
 
277
- ### Entities
204
+ const attemptId = AuthAttemptId.generate();
205
+ const parsed = AuthAttemptId.fromString('550e8400-e29b-41d4-a716-446655440000');
206
+ ```
278
207
 
279
- Entities are objects with a unique identity that persists over time. Unlike
280
- value objects, they can be mutable and have a lifecycle.
208
+ ---
281
209
 
282
- #### Characteristics
210
+ ## Entities
283
211
 
284
- - **Unique Identity**: Distinguished by a unique identifier (not just value)
285
- - **Lifecycle**: Can be created, modified, and deleted
286
- - **Mutable**: State can change, but identity remains constant
287
- - **Equality by Identity**: Two entities with different properties but the same
288
- ID are equal
212
+ Entities have stable identity. Equality is by `id`, not attributes. Use
213
+ `mutate(updater)` for state changes; it re-freezes and re-validates. Use
214
+ `AggregateId` or extend `DomainID` for custom identity types.
289
215
 
290
- #### Implementation
216
+ ### Example (from `@rineex/authentication` OAuthAuthorization)
291
217
 
292
218
  ```typescript
293
- import { Entity, AggregateId, type EntityProps } from '@rineex/ddd';
294
-
295
- interface OrderItemProps {
296
- productId: string;
297
- quantity: number;
298
- unitPrice: number;
299
- }
219
+ import { Entity, EntityProps, DomainID } from '@rineex/ddd';
300
220
 
301
- class OrderItem extends Entity<AggregateId, OrderItemProps> {
302
- get productId(): string {
303
- return this.props.productId;
304
- }
221
+ // Custom ID extend DomainID for domain-specific identifiers
222
+ class OAuthAuthorizationId extends DomainID {}
305
223
 
306
- get quantity(): number {
307
- return this.props.quantity;
308
- }
224
+ export interface OAuthAuthorizationProps {
225
+ provider: string;
226
+ redirectUri: string;
227
+ scope: readonly string[];
228
+ }
309
229
 
310
- get unitPrice(): number {
311
- return this.props.unitPrice;
230
+ export class OAuthAuthorization extends Entity<
231
+ OAuthAuthorizationId,
232
+ OAuthAuthorizationProps
233
+ > {
234
+ constructor(
235
+ props: EntityProps<OAuthAuthorizationId, OAuthAuthorizationProps>,
236
+ ) {
237
+ super({ ...props });
312
238
  }
313
239
 
314
- get total(): number {
315
- return this.quantity * this.unitPrice;
240
+ toObject(): Record<string, unknown> {
241
+ return {
242
+ id: this.id.value,
243
+ provider: this.props.provider,
244
+ redirectUri: this.props.redirectUri,
245
+ scope: this.props.scope,
246
+ };
316
247
  }
317
248
 
318
- protected validate(): void {
319
- if (this.quantity <= 0) {
320
- throw new Error('Quantity must be greater than zero');
321
- }
322
- if (this.unitPrice < 0) {
323
- throw new Error('Unit price cannot be negative');
249
+ validate(): void {
250
+ if (!this.props.redirectUri.startsWith('https://')) {
251
+ throw new Error('Redirect URI must use HTTPS');
324
252
  }
325
253
  }
326
-
327
- public toObject(): Record<string, unknown> {
328
- return {
329
- id: this.id.toString(),
330
- createdAt: this.createdAt.toISOString(),
331
- productId: this.productId,
332
- quantity: this.quantity,
333
- unitPrice: this.unitPrice,
334
- total: this.total,
335
- };
336
- }
337
254
  }
338
255
 
339
- // Creating an entity
340
- const item = new OrderItem({
341
- id: AggregateId.generate(),
342
- createdAt: new Date(),
256
+ // Usage
257
+ const auth = new OAuthAuthorization({
258
+ id: OAuthAuthorizationId.generate(),
343
259
  props: {
344
- productId: 'prod-123',
345
- quantity: 2,
346
- unitPrice: 29.99,
260
+ provider: 'google',
261
+ redirectUri: 'https://app.example.com/callback',
262
+ scope: ['openid', 'email'],
347
263
  },
348
264
  });
349
-
350
- console.log(item.total); // 59.98
265
+ auth.equals(other); // true iff same id
351
266
  ```
352
267
 
353
- ### Aggregate Roots
354
-
355
- Aggregate Roots are entities that serve as entry points to aggregates. They
356
- enforce invariants, manage transactions, and raise domain events.
268
+ ---
357
269
 
358
- #### Characteristics
270
+ ## Aggregate Roots
359
271
 
360
- - **Boundary**: Define the scope of consistency within a transaction
361
- - **Invariant Enforcement**: Validate rules that involve multiple entities or
362
- value objects
363
- - **Event Publisher**: Raise domain events to notify other parts of the system
364
- - **Transaction Consistency**: All changes within an aggregate should be
365
- persisted atomically
272
+ Aggregate roots extend `Entity` and add domain event support.
366
273
 
367
- #### Implementation
274
+ ### Example (from `aggregate-root.spec.ts`)
368
275
 
369
276
  ```typescript
370
277
  import {
371
278
  AggregateRoot,
372
- AggregateId,
373
279
  DomainEvent,
374
- type DomainEventPayload,
280
+ AggregateId,
281
+ EntityValidationError,
375
282
  } from '@rineex/ddd';
376
283
 
377
- // Define domain events
378
- interface UserCreatedPayload extends DomainEventPayload {
379
- email: string;
380
- }
381
-
382
- class UserCreatedEvent extends DomainEvent<AggregateId, UserCreatedPayload> {
383
- public readonly eventName = 'UserCreated';
384
- }
385
-
386
- interface UserEmailChangedPayload extends DomainEventPayload {
387
- oldEmail: string;
388
- newEmail: string;
284
+ interface OrderProps {
285
+ customerId: string;
286
+ total: number;
389
287
  }
390
288
 
391
- class UserEmailChangedEvent extends DomainEvent<
289
+ class OrderCreatedEvent extends DomainEvent<
392
290
  AggregateId,
393
- UserEmailChangedPayload
291
+ { customerId: string }
394
292
  > {
395
- public readonly eventName = 'UserEmailChanged';
396
- }
293
+ readonly eventName = 'OrderCreated';
397
294
 
398
- // Define the aggregate
399
- interface UserProps {
400
- email: string;
401
- isActive: boolean;
295
+ static create(props: {
296
+ id?: string;
297
+ aggregateId: AggregateId;
298
+ schemaVersion: number;
299
+ occurredAt: number;
300
+ payload: { customerId: string };
301
+ }) {
302
+ return new OrderCreatedEvent(props);
303
+ }
402
304
  }
403
305
 
404
- class User extends AggregateRoot<AggregateId, UserProps> {
405
- get email(): string {
406
- return this.props.email;
407
- }
306
+ class OrderCompletedEvent extends DomainEvent<AggregateId, { total: number }> {
307
+ readonly eventName = 'OrderCompleted';
408
308
 
409
- get isActive(): boolean {
410
- return this.props.isActive;
309
+ static create(props: {
310
+ id?: string;
311
+ aggregateId: AggregateId;
312
+ schemaVersion: number;
313
+ occurredAt: number;
314
+ payload: { total: number };
315
+ }) {
316
+ return new OrderCompletedEvent(props);
411
317
  }
318
+ }
412
319
 
413
- public static create(email: string, id?: AggregateId): User {
414
- const userId = id || AggregateId.generate();
415
- const user = new User({
416
- id: userId,
417
- createdAt: new Date(),
418
- props: { email, isActive: true },
419
- });
320
+ class Order extends AggregateRoot<AggregateId, OrderProps> {
321
+ constructor(params: {
322
+ id: AggregateId;
323
+ createdAt?: Date;
324
+ props: OrderProps;
325
+ }) {
326
+ super(params);
327
+ }
420
328
 
421
- user.addEvent(
422
- new UserCreatedEvent({
423
- aggregateId: userId,
329
+ create(): void {
330
+ this.addEvent(
331
+ OrderCreatedEvent.create({
332
+ aggregateId: this.id,
424
333
  schemaVersion: 1,
425
334
  occurredAt: Date.now(),
426
- payload: { email },
335
+ payload: { customerId: this.props.customerId },
427
336
  }),
428
337
  );
429
-
430
- return user;
431
338
  }
432
339
 
433
- public changeEmail(newEmail: string): void {
434
- // Validate before changing
435
- if (!newEmail.includes('@')) {
436
- throw new Error('Invalid email format');
437
- }
438
-
439
- const oldEmail = this.props.email;
440
- this.props = { ...this.props, email: newEmail };
441
-
340
+ complete(): void {
442
341
  this.addEvent(
443
- new UserEmailChangedEvent({
342
+ OrderCompletedEvent.create({
444
343
  aggregateId: this.id,
445
344
  schemaVersion: 1,
446
345
  occurredAt: Date.now(),
447
- payload: { oldEmail, newEmail },
346
+ payload: { total: this.props.total },
448
347
  }),
449
348
  );
450
349
  }
451
350
 
452
- protected validate(): void {
453
- if (!this.props.email || !this.props.email.includes('@')) {
454
- throw new Error('User must have a valid email');
351
+ validate(): void {
352
+ if (!this.props.customerId?.trim()) {
353
+ throw EntityValidationError.create('Customer ID is required', {});
354
+ }
355
+ if (this.props.total < 0) {
356
+ throw EntityValidationError.create('Total must be non-negative', {});
455
357
  }
456
358
  }
457
359
 
458
- public toObject(): Record<string, unknown> {
360
+ toObject() {
459
361
  return {
460
362
  id: this.id.toString(),
461
363
  createdAt: this.createdAt.toISOString(),
462
- email: this.email,
463
- isActive: this.isActive,
364
+ customerId: this.props.customerId,
365
+ total: this.props.total,
464
366
  };
465
367
  }
466
368
  }
467
369
 
468
370
  // Usage
469
- const user = User.create('john@example.com');
470
- user.changeEmail('jane@example.com');
371
+ const order = new Order({
372
+ id: AggregateId.generate(),
373
+ props: { customerId: 'customer-1', total: 100 },
374
+ });
375
+ order.create();
376
+ order.complete();
471
377
 
472
- const events = user.pullDomainEvents(); // Remove events for publishing
473
- console.log(events); // [UserCreatedEvent, UserEmailChangedEvent]
378
+ order.domainEvents; // readonly copy
379
+ const events = order.pullDomainEvents(); // returns and clears
474
380
  ```
475
381
 
476
- #### Key Methods
477
-
478
- - **`addEvent(event: DomainEvent): void`** - Adds a domain event after
479
- validating invariants
480
- - **`pullDomainEvents(): readonly DomainEvent[]`** - Retrieves and clears all
481
- domain events
482
- - **`validate(): void`** - Abstract method for enforcing aggregate invariants
483
-
484
- ### Domain Events
485
-
486
- Domain Events represent significant things that happened in the domain. They are
487
- immutable records of past events and enable event-driven architectures.
382
+ ---
488
383
 
489
- #### Characteristics
384
+ ## Domain Events
490
385
 
491
- - **Immutable**: Represent facts that have already occurred
492
- - **Self-Describing**: Include all necessary information in the payload
493
- - **Serializable**: Can be persisted and transmitted
494
- - **Versioned**: Schema version allows for evolution
495
- - **Timestamped**: Record when the event occurred
386
+ Events are immutable. Payload must be `Serializable` (primitives, arrays, plain
387
+ objects). `id` is auto-generated if omitted.
496
388
 
497
- #### Implementation
389
+ ### Example (from `domain.event.spec.ts`)
498
390
 
499
391
  ```typescript
500
- import { DomainEvent, type DomainEventPayload, AggregateId } from '@rineex/ddd';
392
+ import { DomainEvent, DomainEventPayload, AggregateId } from '@rineex/ddd';
501
393
 
502
- // Define event payloads (only primitives allowed)
503
- interface OrderPlacedPayload extends DomainEventPayload {
504
- customerId: string;
505
- orderId: string;
506
- totalAmount: number;
507
- itemCount: number;
394
+ interface TestPayload extends DomainEventPayload {
395
+ userId: string;
396
+ action: string;
508
397
  }
509
398
 
510
- // Create event class
511
- class OrderPlacedEvent extends DomainEvent<AggregateId, OrderPlacedPayload> {
512
- public readonly eventName = 'OrderPlaced';
399
+ class TestDomainEvent extends DomainEvent<AggregateId, TestPayload> {
400
+ readonly eventName = 'TestEvent';
401
+
402
+ static create(props: {
403
+ id?: string;
404
+ aggregateId: AggregateId;
405
+ schemaVersion: number;
406
+ occurredAt: number;
407
+ payload: TestPayload;
408
+ }) {
409
+ return new TestDomainEvent(props);
410
+ }
513
411
  }
514
412
 
515
- // Using events
516
- const orderId = AggregateId.generate();
517
- const event = new OrderPlacedEvent({
518
- aggregateId: orderId,
413
+ // Usage
414
+ const event = TestDomainEvent.create({
415
+ aggregateId: AggregateId.generate(),
519
416
  schemaVersion: 1,
520
417
  occurredAt: Date.now(),
521
- payload: {
522
- customerId: 'cust-456',
523
- orderId: orderId.toString(),
524
- totalAmount: 99.99,
525
- itemCount: 3,
526
- },
418
+ payload: { userId: 'user-1', action: 'login' },
527
419
  });
528
420
 
529
- // Events are serializable
530
- const primitives = event.toPrimitives();
531
- // {
532
- // id: '...',
533
- // aggregateId: '...',
534
- // schemaVersion: 1,
535
- // occurredAt: 1234567890,
536
- // eventName: 'OrderPlaced',
537
- // payload: { customerId: '...', orderId: '...', ... }
538
- // }
539
- ```
421
+ event.id;
422
+ event.eventName;
423
+ event.aggregateId;
424
+ event.schemaVersion;
425
+ event.occurredAt;
426
+ event.payload;
540
427
 
541
- ### Application Services
428
+ event.toPrimitives();
429
+ // { id, eventName, aggregateId, schemaVersion, occurredAt, payload }
430
+ ```
542
431
 
543
- Application Services orchestrate the business logic of the domain. They are the
544
- entry points for handling use cases and commands.
432
+ ---
545
433
 
546
- #### Characteristics
434
+ ## Domain Errors
547
435
 
548
- - **Use Case Implementation**: Each service handles a single, well-defined use
549
- case
550
- - **Port Interface**: Implement a standard interface for consistency
551
- - **Orchestration**: Coordinate domain objects, repositories, and external
552
- services
553
- - **Transaction Management**: Define transaction boundaries
554
- - **Error Handling**: Map domain errors to application-level responses
436
+ ### Base DomainError
555
437
 
556
- #### Implementation
438
+ Extend `DomainError<Meta, Code>` with `code`, `type`, and constructor
439
+ `super(message, metadata)`.
557
440
 
558
441
  ```typescript
559
- import type { ApplicationServicePort } from '@rineex/ddd';
560
-
561
- // Define input and output DTOs
562
- interface CreateUserInput {
563
- email: string;
564
- name: string;
565
- }
566
-
567
- interface CreateUserOutput {
568
- id: string;
569
- email: string;
570
- name: string;
571
- createdAt: string;
572
- }
573
-
574
- // Implement the service
575
- class CreateUserService implements ApplicationServicePort<
576
- CreateUserInput,
577
- CreateUserOutput
578
- > {
579
- constructor(
580
- private readonly userRepository: UserRepository,
581
- private readonly eventPublisher: EventPublisher,
582
- ) {}
583
-
584
- async execute(args: CreateUserInput): Promise<CreateUserOutput> {
585
- // Check for existing user
586
- const existing = await this.userRepository.findByEmail(args.email);
587
- if (existing) {
588
- throw new Error(`User with email ${args.email} already exists`);
589
- }
442
+ import {
443
+ DomainError,
444
+ DomainErrorCode,
445
+ DomainErrorType,
446
+ Metadata,
447
+ } from '@rineex/ddd';
590
448
 
591
- // Create aggregate
592
- const user = User.create(args.email, args.name);
449
+ type Props = Metadata<{ identityId: string }>;
593
450
 
594
- // Persist
595
- await this.userRepository.save(user);
451
+ class IdentityDisabledError extends DomainError<Props> {
452
+ readonly code: DomainErrorCode = 'AUTH_CORE_IDENTITY.DISABLED_ERROR';
453
+ readonly type: DomainErrorType = 'DOMAIN.INVALID_STATE';
596
454
 
597
- // Publish events
598
- const events = user.pullDomainEvents();
599
- await this.eventPublisher.publishAll(events);
455
+ private constructor(message: string, props: Props) {
456
+ super(message, props);
457
+ }
600
458
 
601
- return {
602
- id: user.id.uuid,
603
- email: user.email,
604
- name: user.name,
605
- createdAt: user.createdAt.toISOString(),
606
- };
459
+ static create(message: string, props: Props) {
460
+ return new IdentityDisabledError(message, props);
607
461
  }
608
462
  }
609
-
610
- // Using the service
611
- const createUserService = new CreateUserService(userRepository, eventPublisher);
612
- const result = await createUserService.execute({
613
- email: 'user@example.com',
614
- name: 'John Doe',
615
- });
616
463
  ```
617
464
 
618
- ## API Reference
619
-
620
- ### Value Objects
621
-
622
- #### `ValueObject<T>`
465
+ ### Extending Error Namespaces
623
466
 
624
- Abstract base class for all value objects.
467
+ Declare namespaces via module augmentation for type-safe codes:
625
468
 
626
469
  ```typescript
627
- export abstract class ValueObject<T> {
628
- get value(): T;
629
- public static is(vo: unknown): vo is ValueObject<unknown>;
630
- public equals(other?: ValueObject<T>): boolean;
631
- protected abstract validate(props: T): void;
632
- }
633
- ```
634
-
635
- **Methods:**
636
-
637
- - `value` - Returns the immutable properties
638
- - `is(vo)` - Type guard for runtime checking
639
- - `equals(other)` - Deep equality comparison
640
- - `validate(props)` - Validation logic (must be implemented)
470
+ // your-module.d.ts
471
+ import '@rineex/ddd';
641
472
 
642
- ### Entities
643
-
644
- #### `Entity<ID extends EntityId, Props>`
645
-
646
- Abstract base class for domain entities.
647
-
648
- ```typescript
649
- export abstract class Entity<ID extends EntityId, Props> {
650
- readonly id: ID;
651
- readonly createdAt: Date;
652
- abstract validate(): void;
653
- abstract toObject(): Record<string, unknown>;
654
- equals(other?: Entity<ID, Props>): boolean;
473
+ declare module '@rineex/ddd' {
474
+ interface DomainErrorNamespaces {
475
+ USER: ['NOT_FOUND', 'INVALID_EMAIL'];
476
+ ORDER: ['NOT_FOUND', 'INVALID_STATUS'];
477
+ }
655
478
  }
656
479
  ```
657
480
 
658
- **Constructor:**
481
+ ### Built-in Errors
659
482
 
660
- ```typescript
661
- new Entity({
662
- id: ID; // Required - must be an EntityId type
663
- createdAt?: Date; // Optional - defaults to new Date()
664
- props: Props; // Domain-specific properties
665
- })
666
- ```
483
+ | Error | Code | Use case |
484
+ | ------------------------- | ------------------------ | ------------------------------------ |
485
+ | `InvalidValueObjectError` | `DOMAIN.INVALID_VALUE` | Value object validation failure |
486
+ | `EntityValidationError` | `CORE.VALIDATION_FAILED` | Entity/aggregate invariant violation |
487
+ | `InvalidValueError` | `DOMAIN.INVALID_VALUE` | Value constraint violation |
488
+ | `InvalidStateError` | `DOMAIN.INVALID_STATE` | Invalid state for operation |
489
+ | `InternalError` | `CORE.INTERNAL_ERROR` | Unexpected/programming errors |
490
+ | `TimeoutError` | `SYSTEM.TIMEOUT` | Operation timeout |
491
+ | `ApplicationError` | (extends `Error`) | Application/HTTP layer errors |
667
492
 
668
- ### Aggregate Roots
493
+ ```typescript
494
+ // InvalidValueError – optional metadata
495
+ throw new InvalidValueError('Age cannot be negative');
496
+ throw new InvalidValueError('Validation failed', {
497
+ field: 'age',
498
+ min: 18,
499
+ max: 100,
500
+ });
669
501
 
670
- #### `AggregateRoot<ID extends EntityId, P>`
502
+ // InvalidStateError no metadata
503
+ throw new InvalidStateError('Cannot cancel completed order');
671
504
 
672
- Extends `Entity` with domain event support.
505
+ // EntityValidationError props required
506
+ throw EntityValidationError.create('Name is required', {});
673
507
 
674
- ```typescript
675
- export abstract class AggregateRoot<ID extends EntityId, P> extends Entity<
676
- ID,
677
- P
678
- > {
679
- readonly domainEvents: readonly DomainEvent[];
680
- abstract validate(): void;
681
- abstract toObject(): Record<string, unknown>;
682
- addEvent(event: DomainEvent): void;
683
- pullDomainEvents(): readonly DomainEvent[];
508
+ // ApplicationError – structured params
509
+ class UserNotFoundError extends ApplicationError {
510
+ constructor(userId: string) {
511
+ super({
512
+ message: `User ${userId} not found`,
513
+ code: 'USER_NOT_FOUND',
514
+ isOperational: true,
515
+ metadata: { userId },
516
+ });
517
+ }
684
518
  }
685
519
  ```
686
520
 
687
- **Methods:**
688
-
689
- - `addEvent(event)` - Add an event after validating invariants
690
- - `pullDomainEvents()` - Get and clear all recorded events
691
- - `domainEvents` - Read-only view of current events
521
+ ---
692
522
 
693
- ### Domain Events
523
+ ## Result Type
694
524
 
695
- #### `DomainEvent<AggregateId extends EntityId, T extends DomainEventPayload>`
525
+ `Result<T, E>` for explicit success/failure without throwing. Default error type
526
+ is `DomainError`.
696
527
 
697
- Abstract base class for domain events.
528
+ ### Example (from `result.spec.ts`)
698
529
 
699
530
  ```typescript
700
- export abstract class DomainEvent<
701
- AggregateId extends EntityId = EntityId,
702
- T extends DomainEventPayload = DomainEventPayload,
703
- > {
704
- abstract readonly eventName: string;
705
- readonly id: string;
706
- readonly aggregateId: AggregateId;
707
- readonly schemaVersion: number;
708
- readonly occurredAt: number;
709
- readonly payload: Readonly<T>;
710
-
711
- toPrimitives(): {
712
- id: string;
713
- aggregateId: string;
714
- schemaVersion: number;
715
- occurredAt: number;
716
- eventName: string;
717
- payload: T;
718
- };
719
- }
720
- ```
531
+ import {
532
+ Result,
533
+ InvalidValueError,
534
+ InvalidStateError,
535
+ DomainError,
536
+ } from '@rineex/ddd';
721
537
 
722
- ### Application Services
538
+ // Creation
539
+ const ok = Result.ok(42);
540
+ const fail = Result.fail(new InvalidValueError('Invalid'));
723
541
 
724
- #### `ApplicationServicePort<I, O>`
542
+ // Checks
543
+ ok.isSuccess; // true
544
+ fail.isFailure; // true
725
545
 
726
- Interface for application services.
546
+ // Extraction
547
+ ok.getValue(); // 42
548
+ fail.getError(); // InvalidValueError
727
549
 
728
- ```typescript
729
- export interface ApplicationServicePort<I, O> {
730
- execute: (args: I) => Promise<O>;
550
+ // Type guards
551
+ if (result.isSuccessResult()) {
552
+ const v = result.getValue(); // T
553
+ }
554
+ if (result.isFailureResult()) {
555
+ const e = result.getError(); // E
731
556
  }
732
557
  ```
733
558
 
734
- ### Value Objects (Pre-built)
735
-
736
- #### `AggregateId`
737
-
738
- Represents the unique identifier for an aggregate. Extends `UUID` which extends
739
- `PrimitiveValueObject<string>`.
559
+ ### Validation pattern
740
560
 
741
561
  ```typescript
742
- class AggregateId extends UUID {
743
- public static generate(): AggregateId;
744
- public static fromString(value: string): AggregateId;
745
- public getValue(): string;
746
- public toString(): string;
562
+ function validateAge(age: number): Result<number, DomainError> {
563
+ if (age < 0) {
564
+ return Result.fail(new InvalidValueError('Age cannot be negative'));
565
+ }
566
+ if (age > 150) {
567
+ return Result.fail(new InvalidValueError('Age seems unrealistic'));
568
+ }
569
+ return Result.ok(age);
747
570
  }
748
571
  ```
749
572
 
750
- #### `UUID`
751
-
752
- Base class for UUID-based value objects.
573
+ ### Chaining
753
574
 
754
575
  ```typescript
755
- class UUID extends PrimitiveValueObject<string> {
756
- public static generate<T extends typeof UUID>(this: T): InstanceType<T>;
757
- public static fromString(value: string): UUID;
758
- public getValue(): string;
759
- public toString(): string;
576
+ function validateEmail(email: string): Result<string, DomainError> {
577
+ if (!email.includes('@')) {
578
+ return Result.fail(new InvalidValueError('Invalid email format'));
579
+ }
580
+ return Result.ok(email);
760
581
  }
761
- ```
762
582
 
763
- #### `IPAddress`
764
-
765
- Validates IPv4 and IPv6 addresses.
583
+ function createAccount(email: string): Result<{ email: string }, DomainError> {
584
+ const emailResult = validateEmail(email);
585
+ if (emailResult.isFailureResult()) return emailResult;
766
586
 
767
- ```typescript
768
- class IPAddress extends ValueObject<string> {
769
- public static create(value: string): IPAddress;
770
- public get value(): string;
587
+ const validated = emailResult.getValue()!;
588
+ return Result.ok({ email: validated });
771
589
  }
772
590
  ```
773
591
 
774
- #### `Url`
592
+ ---
593
+
594
+ ## Application Services
775
595
 
776
- Validates web URLs.
596
+ Use `ApplicationServicePort<I, O>` for use-case orchestration.
777
597
 
778
598
  ```typescript
779
- class Url extends ValueObject<string> {
780
- public static create(value: string): Url;
781
- public get value(): string;
782
- public get href(): string;
783
- }
784
- ```
599
+ import { ApplicationServicePort, Result } from '@rineex/ddd';
785
600
 
786
- #### `UserAgent`
601
+ interface CreateUserInput {
602
+ name: string;
603
+ email: string;
604
+ }
787
605
 
788
- Parses and validates user agent strings.
606
+ interface CreateUserOutput {
607
+ id: string;
608
+ name: string;
609
+ }
789
610
 
790
- ```typescript
791
- class UserAgent extends ValueObject<string> {
792
- public static create(value: string): UserAgent;
793
- public get value(): string;
794
- public get isMobile(): boolean;
795
- public get isBot(): boolean;
796
- public getProps(): string;
611
+ class CreateUserService implements ApplicationServicePort<
612
+ CreateUserInput,
613
+ CreateUserOutput
614
+ > {
615
+ async execute(args: CreateUserInput): Promise<CreateUserOutput> {
616
+ // validate, create entity, persist, publish events
617
+ return { id: '...', name: args.name };
618
+ }
797
619
  }
798
620
  ```
799
621
 
800
- ### Error Types
622
+ ---
801
623
 
802
- #### `DomainError`
624
+ ## Ports & Utilities
803
625
 
804
- Base class for all domain errors.
626
+ ### ClockPort
805
627
 
806
628
  ```typescript
807
- export class DomainError extends Error {
808
- constructor(message: string);
809
- }
810
- ```
629
+ import type { ClockPort } from '@rineex/ddd';
811
630
 
812
- #### `EntityValidationError`
631
+ const clock: ClockPort = {
632
+ now: () => new Date(),
633
+ };
634
+ ```
813
635
 
814
- Thrown when entity validation fails.
636
+ ### HttpStatus & HttpStatusMessage
815
637
 
816
638
  ```typescript
817
- export class EntityValidationError extends DomainError {}
818
- ```
639
+ import { HttpStatus, HttpStatusMessage } from '@rineex/ddd';
819
640
 
820
- #### `InvalidValueObjectError`
641
+ HttpStatus.OK; // 200
642
+ HttpStatus.NOT_FOUND; // 404
643
+ HttpStatusMessage[404]; // 'Not Found'
644
+ ```
821
645
 
822
- Thrown when value object validation fails.
646
+ ### deepFreeze
823
647
 
824
648
  ```typescript
825
- export class InvalidValueObjectError extends DomainError {}
649
+ import { deepFreeze } from '@rineex/ddd';
650
+
651
+ const frozen = deepFreeze({ a: 1, nested: { b: 2 } });
826
652
  ```
827
653
 
828
- #### `ApplicationError`
654
+ ---
655
+
656
+ ## Integration Guide
829
657
 
830
- Thrown for application-level errors with HTTP status codes.
658
+ 1. **Add dependency:** `pnpm add @rineex/ddd`
659
+
660
+ 2. **Extend `DomainErrorNamespaces`** in a `.d.ts` file:
831
661
 
832
662
  ```typescript
833
- export abstract class ApplicationError extends Error {
834
- readonly code: HttpStatusMessage;
835
- readonly status: HttpStatusCode;
836
- readonly isOperational: boolean;
837
- readonly metadata?: Record<string, unknown>;
838
- readonly cause?: Error;
663
+ declare module '@rineex/ddd' {
664
+ interface DomainErrorNamespaces {
665
+ MY_MODULE: ['NOT_FOUND', 'INVALID_INPUT'];
666
+ }
839
667
  }
840
668
  ```
841
669
 
842
- ### Result Type
670
+ 3. **Custom IDs:** Extend `DomainID` and use `generate()` / `fromString()`.
843
671
 
844
- #### `Result<T, E>`
672
+ 4. **Use `mutate()`** for entity/aggregate state changes.
845
673
 
846
- Represents the outcome of an operation that can either succeed or fail, without
847
- relying on exceptions for control flow. This is a functional programming pattern
848
- that makes error handling explicit in the type system and is commonly used in
849
- Domain-Driven Design to represent domain operation outcomes.
674
+ 5. **Persist then publish:** Save aggregate, then call `pullDomainEvents()` and
675
+ publish.
850
676
 
851
- **Key Features:**
677
+ ---
852
678
 
853
- - **Immutable**: Result instances are frozen to prevent accidental mutations
854
- - **Type-Safe**: Full TypeScript support with generic types
855
- - **Explicit Error Handling**: No hidden exceptions, all errors are explicit
856
- - **DomainError Integration**: Works seamlessly with `DomainError` by default
679
+ ## API Reference
857
680
 
858
- ```typescript
859
- export class Result<T, E = DomainError> {
860
- readonly isSuccess: boolean;
861
- readonly isFailure: boolean;
862
-
863
- public static ok<T, E = never>(value: T): Result<T, E>;
864
- public static fail<T = never, E = DomainError>(error: E): Result<T, E>;
865
- public getValue(): T | undefined;
866
- public getError(): E | undefined;
867
- public isSuccessResult(): this is Result<T, never>;
868
- public isFailureResult(): this is Result<never, E>;
869
- }
870
- ```
871
-
872
- **Basic Example:**
873
-
874
- ```typescript
875
- import { Result, DomainError } from '@rineex/ddd';
876
-
877
- class InvalidValueError extends DomainError {
878
- public get code() {
879
- return 'DOMAIN.INVALID_VALUE' as const;
880
- }
881
-
882
- constructor(message: string) {
883
- super({ message });
884
- }
885
- }
886
-
887
- function parseNumber(input: string): Result<number, DomainError> {
888
- const value = Number(input);
889
- if (Number.isNaN(value)) {
890
- return Result.fail(new InvalidValueError('Invalid number'));
891
- }
892
- return Result.ok(value);
893
- }
894
-
895
- const result = parseNumber('42');
896
- if (result.isSuccess) {
897
- const value = result.getValue(); // number | undefined
898
- console.log(value); // 42
899
- } else {
900
- const error = result.getError(); // DomainError | undefined
901
- console.error(error?.message);
902
- }
903
- ```
904
-
905
- **Validation Pattern:**
906
-
907
- ```typescript
908
- function validateAge(age: number): Result<number, DomainError> {
909
- if (age < 0) {
910
- return Result.fail(new InvalidValueError('Age cannot be negative'));
911
- }
912
- if (age > 150) {
913
- return Result.fail(new InvalidValueError('Age seems unrealistic'));
914
- }
915
- return Result.ok(age);
916
- }
917
-
918
- const result = validateAge(25);
919
- if (result.isSuccess) {
920
- console.log('Valid age:', result.getValue());
921
- }
922
- ```
923
-
924
- **Chaining Pattern:**
925
-
926
- ```typescript
927
- function validateEmail(email: string): Result<string, DomainError> {
928
- if (!email.includes('@')) {
929
- return Result.fail(new InvalidValueError('Invalid email format'));
930
- }
931
- return Result.ok(email);
932
- }
933
-
934
- function createAccount(email: string): Result<{ email: string }, DomainError> {
935
- const emailResult = validateEmail(email);
936
- if (emailResult.isFailureResult()) {
937
- return emailResult; // Forward the error
938
- }
939
-
940
- const validatedEmail = emailResult.getValue()!;
941
- return Result.ok({ email: validatedEmail });
942
- }
943
- ```
944
-
945
- **Using Type Guards:**
946
-
947
- The `isSuccessResult()` and `isFailureResult()` methods provide type-safe
948
- narrowing:
949
-
950
- ```typescript
951
- function processResult<T>(result: Result<T, DomainError>): void {
952
- if (result.isSuccessResult()) {
953
- // TypeScript narrows result to Result<T, never>
954
- const value = result.getValue();
955
- if (value) {
956
- // Process the value with full type safety
957
- console.log('Success:', value);
958
- }
959
- } else if (result.isFailureResult()) {
960
- // TypeScript narrows result to Result<never, DomainError>
961
- const error = result.getError();
962
- if (error) {
963
- // Access error properties with full type safety
964
- console.error('Error code:', error.code);
965
- console.error('Error message:', error.message);
966
- }
967
- }
968
- }
969
- ```
970
-
971
- **Working with Domain Errors:**
972
-
973
- ```typescript
974
- class InvalidStateError extends DomainError {
975
- public get code() {
976
- return 'DOMAIN.INVALID_STATE' as const;
977
- }
978
-
979
- constructor(
980
- message: string,
981
- metadata?: Record<string, boolean | number | string>,
982
- ) {
983
- super({ message, metadata });
984
- }
985
- }
986
-
987
- function processOrder(orderId: string): Result<Order, DomainError> {
988
- const order = orderRepository.findById(orderId);
989
- if (!order) {
990
- return Result.fail(new InvalidValueError('Order not found', { orderId }));
991
- }
992
- if (order.status !== 'PENDING') {
993
- return Result.fail(
994
- new InvalidStateError('Order cannot be processed', {
995
- orderId,
996
- currentStatus: order.status,
997
- }),
998
- );
999
- }
1000
- return Result.ok(order);
1001
- }
1002
- ```
1003
-
1004
- **Void Operations:**
1005
-
1006
- ```typescript
1007
- function deleteUser(id: number): Result<void, DomainError> {
1008
- if (id <= 0) {
1009
- return Result.fail(new InvalidValueError('Invalid user ID'));
1010
- }
1011
- // ... deletion logic ...
1012
- return Result.ok(undefined);
1013
- }
1014
- ```
1015
-
1016
- **Best Practices:**
1017
-
1018
- 1. Always check `isSuccess` or `isFailure` before calling `getValue()` or
1019
- `getError()`
1020
- 2. Use `isSuccessResult()` and `isFailureResult()` type guards for better type
1021
- narrowing when you need TypeScript to narrow the result type
1022
- 3. Use `DomainError` for domain-specific errors to maintain consistency
1023
- 4. Forward errors in chaining operations rather than creating new ones
1024
- 5. Leverage TypeScript's type narrowing for safe value extraction
1025
-
1026
- ### Domain Violations
1027
-
1028
- #### `DomainViolation`
1029
-
1030
- Base class for domain violations. Purposely does NOT extend native Error to
1031
- avoid stack trace overhead in the domain.
1032
-
1033
- ```typescript
1034
- export abstract class DomainViolation {
1035
- abstract readonly code: string;
1036
- abstract readonly message: string;
1037
- readonly metadata: Readonly<Record<string, unknown>>;
1038
- }
1039
- ```
1040
-
1041
- ### HTTP Status Codes
1042
-
1043
- #### `HttpStatus` and `HttpStatusMessage`
1044
-
1045
- Typed HTTP status code constants and messages.
1046
-
1047
- ```typescript
1048
- export const HttpStatus = {
1049
- OK: 200,
1050
- CREATED: 201,
1051
- BAD_REQUEST: 400,
1052
- UNAUTHORIZED: 401,
1053
- FORBIDDEN: 403,
1054
- NOT_FOUND: 404,
1055
- INTERNAL_SERVER_ERROR: 500,
1056
- // ... and many more
1057
- } as const;
1058
-
1059
- export const HttpStatusMessage = {
1060
- 200: 'OK',
1061
- 201: 'Created',
1062
- 400: 'Bad Request',
1063
- // ... and many more
1064
- } as const;
1065
-
1066
- export type HttpStatusCode = keyof typeof HttpStatusMessage;
1067
- export type HttpStatusMessage =
1068
- (typeof HttpStatusMessage)[keyof typeof HttpStatusMessage];
1069
- ```
1070
-
1071
- ### Utilities
1072
-
1073
- #### `unwrapValueObject<T>`
1074
-
1075
- Recursively unwraps value objects from nested structures.
1076
-
1077
- ```typescript
1078
- export function unwrapValueObject<T>(
1079
- input: T,
1080
- seen?: WeakSet<object>
1081
- ): UnwrapValueObject<T>;
1082
-
1083
- export type UnwrapValueObject<T> = /* recursive type utility */;
1084
- ```
1085
-
1086
- #### `deepFreeze<T>`
1087
-
1088
- Deeply freezes objects to ensure immutability.
1089
-
1090
- ```typescript
1091
- export function deepFreeze<T>(obj: T, seen?: WeakSet<object>): Readonly<T>;
1092
- ```
1093
-
1094
- ### Types
1095
-
1096
- #### `EntityId<T>`
1097
-
1098
- Interface for identity value objects.
1099
-
1100
- ```typescript
1101
- export interface EntityId<T = string> {
1102
- equals(other?: EntityId<T> | null | undefined): boolean;
1103
- toString(): string;
1104
- }
1105
- ```
1106
-
1107
- #### `EntityProps<ID extends EntityId, Props>`
1108
-
1109
- Configuration for the base Entity constructor.
1110
-
1111
- ```typescript
1112
- export interface EntityProps<ID extends EntityId, Props> {
1113
- readonly id: ID;
1114
- readonly createdAt?: Date;
1115
- readonly props: Props;
1116
- }
1117
- ```
1118
-
1119
- #### `DomainEventPayload`
1120
-
1121
- Type for domain event payloads (only primitives and serializable structures).
1122
-
1123
- ```typescript
1124
- export type DomainEventPayload = Record<string, Serializable>;
1125
- ```
1126
-
1127
- ## Examples
1128
-
1129
- ### Complete Order Management System
1130
-
1131
- Here's a realistic example showing how to structure a domain with multiple
1132
- aggregates:
1133
-
1134
- ```typescript
1135
- import {
1136
- AggregateRoot,
1137
- AggregateId,
1138
- ValueObject,
1139
- DomainEvent,
1140
- Entity,
1141
- ApplicationServicePort,
1142
- type DomainEventPayload,
1143
- } from '@rineex/ddd';
1144
-
1145
- // ============ Value Objects ============
1146
-
1147
- interface MoneyProps {
1148
- amount: number;
1149
- currency: string;
1150
- }
1151
-
1152
- class Money extends ValueObject<MoneyProps> {
1153
- get amount(): number {
1154
- return this.props.amount;
1155
- }
1156
-
1157
- get currency(): string {
1158
- return this.props.currency;
1159
- }
1160
-
1161
- public static create(amount: number, currency = 'USD'): Money {
1162
- return new Money({ amount, currency });
1163
- }
1164
-
1165
- protected validate(props: MoneyProps): void {
1166
- if (props.amount < 0) {
1167
- throw new Error('Amount cannot be negative');
1168
- }
1169
- if (!props.currency || props.currency.length !== 3) {
1170
- throw new Error('Invalid currency code');
1171
- }
1172
- }
1173
- }
1174
-
1175
- // ============ Entities ============
1176
-
1177
- interface OrderLineProps {
1178
- productId: string;
1179
- quantity: number;
1180
- price: Money;
1181
- }
1182
-
1183
- class OrderLine extends Entity<AggregateId, OrderLineProps> {
1184
- get productId(): string {
1185
- return this.props.productId;
1186
- }
1187
-
1188
- get quantity(): number {
1189
- return this.props.quantity;
1190
- }
1191
-
1192
- get price(): Money {
1193
- return this.props.price;
1194
- }
1195
-
1196
- get subtotal(): Money {
1197
- return Money.create(this.price.amount * this.quantity, this.price.currency);
1198
- }
1199
-
1200
- protected validate(): void {
1201
- if (this.quantity <= 0) {
1202
- throw new Error('Quantity must be positive');
1203
- }
1204
- }
1205
-
1206
- public toObject(): Record<string, unknown> {
1207
- return {
1208
- id: this.id.toString(),
1209
- createdAt: this.createdAt.toISOString(),
1210
- productId: this.productId,
1211
- quantity: this.quantity,
1212
- price: this.price.value,
1213
- };
1214
- }
1215
- }
1216
-
1217
- // ============ Domain Events ============
1218
-
1219
- interface OrderCreatedPayload extends DomainEventPayload {
1220
- customerId: string;
1221
- }
1222
-
1223
- class OrderCreatedEvent extends DomainEvent<AggregateId, OrderCreatedPayload> {
1224
- public readonly eventName = 'OrderCreated';
1225
- }
1226
-
1227
- interface OrderLineAddedPayload extends DomainEventPayload {
1228
- productId: string;
1229
- quantity: number;
1230
- }
1231
-
1232
- class OrderLineAddedEvent extends DomainEvent<
1233
- AggregateId,
1234
- OrderLineAddedPayload
1235
- > {
1236
- public readonly eventName = 'OrderLineAdded';
1237
- }
1238
-
1239
- interface OrderCompletedPayload extends DomainEventPayload {
1240
- total: number;
1241
- }
1242
-
1243
- class OrderCompletedEvent extends DomainEvent<
1244
- AggregateId,
1245
- OrderCompletedPayload
1246
- > {
1247
- public readonly eventName = 'OrderCompleted';
1248
- }
1249
-
1250
- // ============ Aggregate Root ============
1251
-
1252
- interface OrderProps {
1253
- customerId: string;
1254
- lines: OrderLine[];
1255
- status: 'pending' | 'completed' | 'cancelled';
1256
- total: Money;
1257
- }
1258
-
1259
- class Order extends AggregateRoot<AggregateId, OrderProps> {
1260
- get customerId(): string {
1261
- return this.props.customerId;
1262
- }
1263
-
1264
- get lines(): OrderLine[] {
1265
- return this.props.lines;
1266
- }
1267
-
1268
- get status(): string {
1269
- return this.props.status;
1270
- }
1271
-
1272
- get total(): Money {
1273
- return this.props.total;
1274
- }
681
+ ### ValueObject\<T\>
682
+
683
+ | Member | Description |
684
+ | -------------------- | ------------------------ |
685
+ | `value` | Read-only props |
686
+ | `equals(other)` | Deep equality |
687
+ | `toJSON()` | Returns props |
688
+ | `toString()` | `JSON.stringify(props)` |
689
+ | `ValueObject.is(vo)` | Type guard |
690
+ | `validate(props)` | Abstract, must implement |
691
+
692
+ ### PrimitiveValueObject\<T\>
693
+
694
+ | Member | Description |
695
+ | ----------------- | --------------------- |
696
+ | `value` | Primitive value |
697
+ | `getValue()` | Same (deprecated) |
698
+ | `equals(other)` | Reference equality |
699
+ | `toString()` | String representation |
700
+ | `validate(value)` | Abstract |
701
+
702
+ ### Entity\<ID, Props\>
703
+
704
+ | Member | Description |
705
+ | ----------------- | ------------------------------ |
706
+ | `id` | Identity |
707
+ | `createdAt` | Creation date |
708
+ | `props` | Read-only (protected) |
709
+ | `equals(other)` | By `id` |
710
+ | `mutate(updater)` | Safe state change + revalidate |
711
+ | `validate()` | Abstract |
712
+ | `toObject()` | Abstract |
713
+
714
+ ### AggregateRoot\<ID, Props\>
715
+
716
+ Extends `Entity`. Adds:
717
+
718
+ | Member | Description |
719
+ | -------------------- | ------------------- |
720
+ | `addEvent(event)` | Append domain event |
721
+ | `domainEvents` | Read-only copy |
722
+ | `pullDomainEvents()` | Return and clear |
723
+
724
+ ### DomainEvent\<AggregateId, Payload\>
725
+
726
+ | Member | Description |
727
+ | ---------------- | ------------------- |
728
+ | `id` | Event ID |
729
+ | `aggregateId` | Aggregate reference |
730
+ | `schemaVersion` | Version |
731
+ | `occurredAt` | Unix ms |
732
+ | `payload` | Serializable data |
733
+ | `eventName` | Abstract |
734
+ | `toPrimitives()` | Plain object |
735
+
736
+ ### Result\<T, E\>
737
+
738
+ | Member | Description |
739
+ | ---------------------------------------- | -------------- |
740
+ | `Result.ok(value)` | Success |
741
+ | `Result.fail(err)` | Failure |
742
+ | `isSuccess`, `isFailure` | Booleans |
743
+ | `getValue()`, `getError()` | Value or error |
744
+ | `isSuccessResult()`, `isFailureResult()` | Type guards |
1275
745
 
1276
- public static create(customerId: string, id?: AggregateId): Order {
1277
- const orderId = id || AggregateId.generate();
1278
- const order = new Order({
1279
- id: orderId,
1280
- createdAt: new Date(),
1281
- props: {
1282
- customerId,
1283
- lines: [],
1284
- status: 'pending',
1285
- total: Money.create(0),
1286
- },
1287
- });
1288
-
1289
- order.addEvent(
1290
- new OrderCreatedEvent({
1291
- aggregateId: orderId,
1292
- schemaVersion: 1,
1293
- occurredAt: Date.now(),
1294
- payload: { customerId },
1295
- }),
1296
- );
1297
-
1298
- return order;
1299
- }
1300
-
1301
- public addLine(productId: string, quantity: number, price: Money): void {
1302
- const line = new OrderLine({
1303
- id: AggregateId.generate(),
1304
- createdAt: new Date(),
1305
- props: { productId, quantity, price },
1306
- });
1307
-
1308
- this.props.lines.push(line);
1309
- this.recalculateTotal();
1310
-
1311
- this.addEvent(
1312
- new OrderLineAddedEvent({
1313
- aggregateId: this.id,
1314
- schemaVersion: 1,
1315
- occurredAt: Date.now(),
1316
- payload: { productId, quantity },
1317
- }),
1318
- );
1319
- }
1320
-
1321
- public complete(): void {
1322
- if (this.status !== 'pending') {
1323
- throw new Error('Only pending orders can be completed');
1324
- }
1325
-
1326
- this.props.status = 'completed';
1327
-
1328
- this.addEvent(
1329
- new OrderCompletedEvent({
1330
- aggregateId: this.id,
1331
- schemaVersion: 1,
1332
- occurredAt: Date.now(),
1333
- payload: { total: this.total.amount },
1334
- }),
1335
- );
1336
- }
1337
-
1338
- private recalculateTotal(): void {
1339
- const sum = this.lines.reduce((acc, line) => acc + line.subtotal.amount, 0);
1340
- this.props.total = Money.create(sum, 'USD');
1341
- }
1342
-
1343
- protected validate(): void {
1344
- if (!this.customerId) {
1345
- throw new Error('Customer ID is required');
1346
- }
1347
- if (this.lines.length === 0) {
1348
- throw new Error('Order must have at least one line');
1349
- }
1350
- }
1351
-
1352
- public toObject(): Record<string, unknown> {
1353
- return {
1354
- id: this.id.toString(),
1355
- createdAt: this.createdAt.toISOString(),
1356
- customerId: this.customerId,
1357
- lines: this.lines.map(line => line.toObject()),
1358
- status: this.status,
1359
- total: this.total.value,
1360
- };
1361
- }
1362
- }
1363
-
1364
- // ============ Application Service ============
1365
-
1366
- interface CreateOrderInput {
1367
- customerId: string;
1368
- lines: { productId: string; quantity: number; price: number }[];
1369
- }
1370
-
1371
- interface CreateOrderOutput {
1372
- id: string;
1373
- customerId: string;
1374
- total: number;
1375
- lineCount: number;
1376
- }
1377
-
1378
- class CreateOrderService implements ApplicationServicePort<
1379
- CreateOrderInput,
1380
- CreateOrderOutput
1381
- > {
1382
- constructor(private readonly orderRepository: OrderRepository) {}
1383
-
1384
- async execute(args: CreateOrderInput): Promise<CreateOrderOutput> {
1385
- const order = Order.create(args.customerId);
1386
-
1387
- for (const line of args.lines) {
1388
- order.addLine(line.productId, line.quantity, Money.create(line.price));
1389
- }
1390
-
1391
- order.complete();
1392
- await this.orderRepository.save(order);
1393
-
1394
- return {
1395
- id: order.id.toString(),
1396
- customerId: order.customerId,
1397
- total: order.total.amount,
1398
- lineCount: order.lines.length,
1399
- };
1400
- }
1401
- }
1402
- ```
1403
-
1404
- ## Best Practices
1405
-
1406
- ### 1. **Make Invalid States Impossible**
1407
-
1408
- Use type system and validation to make invalid states impossible to construct:
1409
-
1410
- ```typescript
1411
- // ❌ BAD: Can create invalid state
1412
- class User {
1413
- email: string;
1414
- isVerified: boolean;
1415
- }
1416
-
1417
- // ✅ GOOD: Invalid state impossible
1418
- class UnverifiedUser extends ValueObject<{ email: string }> {}
1419
- class VerifiedUser extends ValueObject<{ email: string; verifiedAt: Date }> {}
1420
- ```
1421
-
1422
- ### 2. **Keep Aggregates Small**
1423
-
1424
- Prefer small aggregates with clear boundaries over large aggregates with many
1425
- entities:
1426
-
1427
- ```typescript
1428
- // ❌ BAD: Too many entities in one aggregate
1429
- class Store extends AggregateRoot {
1430
- employees: Employee[];
1431
- inventory: InventoryItem[];
1432
- orders: Order[];
1433
- // ... many more
1434
- }
1435
-
1436
- // ✅ GOOD: Separate aggregates with references
1437
- class Store extends AggregateRoot {
1438
- name: string;
1439
- // Reference to other aggregates by ID only
1440
- employeeIds: AggregateId[];
1441
- }
1442
-
1443
- class Inventory extends AggregateRoot {
1444
- storeId: AggregateId;
1445
- items: InventoryItem[];
1446
- }
1447
- ```
1448
-
1449
- ### 3. **Use Value Objects for Primitive Types**
1450
-
1451
- Wrap primitives that have domain meaning:
1452
-
1453
- ```typescript
1454
- // ❌ BAD: Raw primitive types
1455
- interface User {
1456
- email: string;
1457
- phone: string;
1458
- age: number;
1459
- }
1460
-
1461
- // ✅ GOOD: Domain-meaningful value objects
1462
- interface User {
1463
- email: Email;
1464
- phone: PhoneNumber;
1465
- age: Age;
1466
- }
1467
- ```
1468
-
1469
- ### 4. **Validate at Boundaries**
1470
-
1471
- Perform all validation when creating aggregates, not repeatedly:
1472
-
1473
- ```typescript
1474
- // ❌ BAD: Repeated validation
1475
- function updateEmail(email: string) {
1476
- if (!isValidEmail(email)) throw Error();
1477
- }
1478
-
1479
- function sendEmail(email: string) {
1480
- if (!isValidEmail(email)) throw Error();
1481
- }
1482
-
1483
- // ✅ GOOD: Single validation point
1484
- const email = Email.create(value); // Throws if invalid
1485
- updateEmail(email);
1486
- sendEmail(email);
1487
- ```
1488
-
1489
- ### 5. **Event-Driven State Changes**
1490
-
1491
- All changes should be reflected in domain events:
1492
-
1493
- ```typescript
1494
- // ✅ GOOD: Changes recorded as events
1495
- class User extends AggregateRoot {
1496
- changeEmail(newEmail: Email): void {
1497
- const oldEmail = this.email;
1498
- this.props.email = newEmail;
1499
-
1500
- this.addEvent(
1501
- new EmailChangedEvent({
1502
- id: crypto.randomUUID(),
1503
- aggregateId: this.id.uuid,
1504
- schemaVersion: 1,
1505
- occurredAt: Date.now(),
1506
- payload: { oldEmail: oldEmail.value, newEmail: newEmail.value },
1507
- }),
1508
- );
1509
- }
1510
- }
1511
- ```
1512
-
1513
- ### 6. **Publish Events After Persistence**
1514
-
1515
- Always publish events after persisting the aggregate:
1516
-
1517
- ```typescript
1518
- async function handle(command: CreateUserCommand): Promise<void> {
1519
- // Create aggregate
1520
- const user = User.create(command.email);
1521
-
1522
- // Persist first
1523
- await userRepository.save(user);
1524
-
1525
- // Then publish
1526
- const events = user.pullDomainEvents();
1527
- await eventPublisher.publishAll(events);
1528
- }
1529
- ```
1530
-
1531
- ### 7. **Immutability by Convention**
1532
-
1533
- Even though TypeScript doesn't enforce it, treat all domain objects as
1534
- immutable:
1535
-
1536
- ```typescript
1537
- // ✅ GOOD: Replace entire aggregate when state changes
1538
- class User extends AggregateRoot {
1539
- changeName(newName: string): void {
1540
- // Don't mutate: this.props.name = newName;
1541
-
1542
- // Instead, create new object:
1543
- this.props = { ...this.props, name: newName };
1544
- }
1545
- }
1546
- ```
1547
-
1548
- ## Error Handling
1549
-
1550
- Handle different error scenarios appropriately:
1551
-
1552
- ```typescript
1553
- import {
1554
- DomainError,
1555
- EntityValidationError,
1556
- InvalidValueObjectError,
1557
- ApplicationError,
1558
- Result,
1559
- } from '@rineex/ddd';
1560
-
1561
- // Using exceptions (traditional approach)
1562
- try {
1563
- const email = Email.create('invalid-email');
1564
- } catch (error) {
1565
- if (error instanceof InvalidValueObjectError) {
1566
- // Handle value object validation errors
1567
- console.error('Invalid email format:', error.message);
1568
- }
1569
- }
1570
-
1571
- try {
1572
- const user = new User({
1573
- id: AggregateId.generate(),
1574
- createdAt: new Date(),
1575
- props: { email: Email.create('user@example.com') },
1576
- });
1577
- user.validate();
1578
- } catch (error) {
1579
- if (error instanceof EntityValidationError) {
1580
- // Handle entity validation errors
1581
- console.error('User invariant violated:', error.message);
1582
- }
1583
- }
1584
-
1585
- try {
1586
- await userService.execute(input);
1587
- } catch (error) {
1588
- if (error instanceof ApplicationError) {
1589
- // Handle application-level errors
1590
- console.error('Service failed:', error.message);
1591
- console.error('HTTP Status:', error.status);
1592
- console.error('Error Code:', error.code);
1593
- } else if (error instanceof DomainError) {
1594
- // Catch-all for domain errors
1595
- console.error('Domain error:', error.message);
1596
- console.error('Error Code:', error.code);
1597
- }
1598
- }
1599
-
1600
- // Using Result type (functional approach with DomainError)
1601
- class InvalidEmailError extends DomainError {
1602
- public get code() {
1603
- return 'DOMAIN.INVALID_VALUE' as const;
1604
- }
1605
-
1606
- constructor(message: string) {
1607
- super({ message });
1608
- }
1609
- }
1610
-
1611
- function createUser(email: string): Result<User, DomainError> {
1612
- // Validate email format
1613
- if (!email.includes('@')) {
1614
- return Result.fail(new InvalidEmailError('Invalid email format'));
1615
- }
1616
-
1617
- try {
1618
- const emailVO = Email.create(email);
1619
- const user = new User({
1620
- id: AggregateId.generate(),
1621
- createdAt: new Date(),
1622
- props: { email: emailVO, isActive: true },
1623
- });
1624
- return Result.ok(user);
1625
- } catch (error) {
1626
- if (error instanceof InvalidValueObjectError) {
1627
- return Result.fail(new InvalidEmailError(error.message));
1628
- }
1629
- return Result.fail(
1630
- new InvalidEmailError(
1631
- error instanceof Error ? error.message : 'Unknown error',
1632
- ),
1633
- );
1634
- }
1635
- }
1636
-
1637
- const result = createUser('user@example.com');
1638
- if (result.isSuccessResult()) {
1639
- const user = result.getValue();
1640
- if (user) {
1641
- // Use user safely with full type safety
1642
- console.log('User created:', user.id.toString());
1643
- }
1644
- } else if (result.isFailureResult()) {
1645
- const error = result.getError();
1646
- if (error) {
1647
- // Handle error with full context and type safety
1648
- console.error('Error code:', error.code);
1649
- console.error('Error message:', error.message);
1650
- console.error('Metadata:', error.metadata);
1651
- }
1652
- }
1653
- ```
1654
-
1655
- ## TypeScript Support
1656
-
1657
- This library is built with TypeScript 5.9+ and provides comprehensive type
1658
- safety:
1659
-
1660
- ```typescript
1661
- // Full type inference
1662
- const user = User.create('user@example.com');
1663
- const id: AggregateId = user.id; // Correctly typed
1664
-
1665
- // Type-safe event handling
1666
- const events = user.pullDomainEvents();
1667
- events.forEach(event => {
1668
- if (event instanceof UserCreatedEvent) {
1669
- // Type guard works correctly
1670
- const payload = event.payload; // Correctly inferred type
1671
- }
1672
- });
1673
-
1674
- // Proper generic constraints
1675
- class MyAggregate extends AggregateRoot<MyProps> {
1676
- // Full type safety with MyProps
1677
- }
1678
- ```
1679
-
1680
- ### Recommended TypeScript Configuration
1681
-
1682
- ```json
1683
- {
1684
- "compilerOptions": {
1685
- "target": "ES2020",
1686
- "module": "ESNext",
1687
- "lib": ["ES2020"],
1688
- "strict": true,
1689
- "skipLibCheck": true,
1690
- "forceConsistentCasingInFileNames": true,
1691
- "resolveJsonModule": true,
1692
- "esModuleInterop": true,
1693
- "declaration": true,
1694
- "declarationMap": true,
1695
- "sourceMap": true
1696
- }
1697
- }
1698
- ```
1699
-
1700
- ## Contributing
1701
-
1702
- Contributions are welcome! Please follow these guidelines:
1703
-
1704
- 1. **Fork** the repository
1705
- 2. **Create** a feature branch (`git checkout -b feature/amazing-feature`)
1706
- 3. **Write** tests for new functionality
1707
- 4. **Ensure** all tests pass (`pnpm test`)
1708
- 5. **Follow** the code style (`pnpm lint`)
1709
- 6. **Commit** with clear messages
1710
- 7. **Push** to the branch and create a Pull Request
1711
-
1712
- ### Development Setup
1713
-
1714
- ```bash
1715
- # Install dependencies
1716
- pnpm install
1717
-
1718
- # Run tests
1719
- pnpm test
1720
-
1721
- # Run linter
1722
- pnpm lint
1723
-
1724
- # Check types
1725
- pnpm check-types
1726
-
1727
- # Build the package
1728
- pnpm build
1729
- ```
1730
-
1731
- ### Code Style
1732
-
1733
- - Follow the existing code style
1734
- - Use TypeScript strict mode
1735
- - Write descriptive variable and function names
1736
- - Add JSDoc comments for public APIs
1737
- - Keep functions small and focused
746
+ ---
1738
747
 
1739
748
  ## License
1740
749
 
1741
- This project is licensed under the Apache License 2.0 - see the
1742
- [LICENSE](LICENSE) file for details.
1743
-
1744
- ## Related Resources
1745
-
1746
- - [Domain-Driven Design: Tackling Complexity in the Heart of Software](https://www.domainlanguage.com/ddd/)
1747
- by Eric Evans
1748
- - [Implementing Domain-Driven Design](https://vaughnvernon.com/books/) by Vaughn
1749
- Vernon
1750
- - [Architecture Patterns with Python](https://www.cosmicpython.com/) by Harry J.
1751
- W. Percival and Bob Gregory
1752
- - [TypeScript Handbook](https://www.typescriptlang.org/docs/)
1753
-
1754
- ## Support
1755
-
1756
- For issues, questions, or suggestions, please open an issue on
1757
- [GitHub](https://github.com/rineex/core/issues).
1758
-
1759
- ---
1760
-
1761
- **Made with ❤️ by the Rineex Team**
750
+ Apache-2.0 see [LICENSE](../../LICENSE).