@el-j/magic-helix-core 4.0.0-beta.2 → 4.0.0-beta.3
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/dist/index-B88j4AyE.js +13 -0
- package/dist/index-B88j4AyE.js.map +1 -0
- package/dist/index-CY-pQbuu.cjs +2 -0
- package/dist/index-CY-pQbuu.cjs.map +1 -0
- package/dist/index.cjs +75 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +2234 -51
- package/dist/index.mjs.map +1 -1
- package/dist/pattern-combiner.d.ts +1 -1
- package/dist/plugin-loader.d.ts +2 -1
- package/package.json +4 -4
- package/dist/BasePlugin-6wv0hYJ9.js +0 -98
- package/dist/BasePlugin-6wv0hYJ9.js.map +0 -1
- package/dist/BasePlugin-odQJAKA-.cjs +0 -2
- package/dist/BasePlugin-odQJAKA-.cjs.map +0 -1
- package/dist/builtin-plugins/base/BasePlugin.d.ts +0 -69
- package/dist/builtin-plugins/cpp/index.d.ts +0 -46
- package/dist/builtin-plugins/csharp/index.d.ts +0 -20
- package/dist/builtin-plugins/go/index.d.ts +0 -23
- package/dist/builtin-plugins/index.d.ts +0 -16
- package/dist/builtin-plugins/java/index.d.ts +0 -22
- package/dist/builtin-plugins/nodejs/index.d.ts +0 -44
- package/dist/builtin-plugins/php/index.d.ts +0 -20
- package/dist/builtin-plugins/python/index.d.ts +0 -27
- package/dist/builtin-plugins/ruby/index.d.ts +0 -20
- package/dist/builtin-plugins/rust/index.d.ts +0 -53
- package/dist/builtin-plugins/swift/index.d.ts +0 -22
- package/dist/default_templates/angular/angular-core.md +0 -19
- package/dist/default_templates/architecture/codeowners.md +0 -123
- package/dist/default_templates/architecture/monorepo.md +0 -146
- package/dist/default_templates/architecture/nx.md +0 -122
- package/dist/default_templates/architecture/turborepo.md +0 -114
- package/dist/default_templates/ci/github-actions.md +0 -268
- package/dist/default_templates/ci/gitlab-ci.md +0 -330
- package/dist/default_templates/containers/docker-multistage.md +0 -120
- package/dist/default_templates/containers/kubernetes-deploy.md +0 -210
- package/dist/default_templates/devops/docker-compose.md +0 -111
- package/dist/default_templates/devops/docker-dockerfile.md +0 -94
- package/dist/default_templates/devops/github-actions.md +0 -160
- package/dist/default_templates/devops/gitlab-ci.md +0 -210
- package/dist/default_templates/dotnet/framework-aspnetcore.md +0 -205
- package/dist/default_templates/dotnet/framework-blazor.md +0 -271
- package/dist/default_templates/dotnet/lang-csharp.md +0 -162
- package/dist/default_templates/generic/lang-typescript.md +0 -57
- package/dist/default_templates/generic/state-redux.md +0 -21
- package/dist/default_templates/generic/state-rxjs.md +0 -6
- package/dist/default_templates/generic/style-mui.md +0 -23
- package/dist/default_templates/generic/style-tailwind.md +0 -76
- package/dist/default_templates/generic/test-cypress.md +0 -21
- package/dist/default_templates/generic/test-jest.md +0 -20
- package/dist/default_templates/generic/test-playwright.md +0 -21
- package/dist/default_templates/generic/test-vitest.md +0 -131
- package/dist/default_templates/go/lang-go.md +0 -571
- package/dist/default_templates/java/build-gradle.md +0 -102
- package/dist/default_templates/java/build-maven.md +0 -86
- package/dist/default_templates/java/framework-spring-boot.md +0 -179
- package/dist/default_templates/java/lang-java.md +0 -78
- package/dist/default_templates/java/lang-kotlin.md +0 -88
- package/dist/default_templates/meta/magic-helix-meta.md +0 -213
- package/dist/default_templates/meta/meta-debug.md +0 -459
- package/dist/default_templates/meta/meta-implement.md +0 -450
- package/dist/default_templates/meta/meta-roadmap.md +0 -265
- package/dist/default_templates/nestjs/nestjs-core.md +0 -7
- package/dist/default_templates/patterns/architecture/clean-architecture.md +0 -469
- package/dist/default_templates/patterns/architecture/dependency-injection.md +0 -517
- package/dist/default_templates/patterns/architecture/domain-driven-design.md +0 -621
- package/dist/default_templates/patterns/architecture/layered-architecture.md +0 -382
- package/dist/default_templates/patterns/architecture/repository-pattern.md +0 -408
- package/dist/default_templates/patterns/domain-expertise/nextjs-rules.md +0 -115
- package/dist/default_templates/patterns/domain-expertise/react-patterns.md +0 -181
- package/dist/default_templates/patterns/domain-expertise/server-components.md +0 -212
- package/dist/default_templates/patterns/domain-expertise/shadcn-ui.md +0 -52
- package/dist/default_templates/patterns/domain-expertise/tailwind-patterns.md +0 -52
- package/dist/default_templates/patterns/environment/container-awareness.md +0 -17
- package/dist/default_templates/patterns/environment/ide-features.md +0 -17
- package/dist/default_templates/patterns/environment/os-commands.md +0 -17
- package/dist/default_templates/patterns/organization/heading-hierarchy.md +0 -103
- package/dist/default_templates/patterns/organization/sequential-workflows.md +0 -102
- package/dist/default_templates/patterns/organization/xml-rule-groups.md +0 -64
- package/dist/default_templates/patterns/reasoning/agent-loop.md +0 -151
- package/dist/default_templates/patterns/reasoning/confirmation-gates.md +0 -141
- package/dist/default_templates/patterns/reasoning/dependency-analysis.md +0 -132
- package/dist/default_templates/patterns/reasoning/one-tool-per-iteration.md +0 -152
- package/dist/default_templates/patterns/reasoning/preview-before-action.md +0 -194
- package/dist/default_templates/patterns/reasoning/reflection-checkpoints.md +0 -166
- package/dist/default_templates/patterns/reasoning/result-verification.md +0 -157
- package/dist/default_templates/patterns/reasoning/subtask-breakdown.md +0 -131
- package/dist/default_templates/patterns/reasoning/thinking-tags.md +0 -100
- package/dist/default_templates/patterns/role-definition/capability-declarations.md +0 -72
- package/dist/default_templates/patterns/role-definition/expert-identity.md +0 -45
- package/dist/default_templates/patterns/role-definition/scope-boundaries.md +0 -61
- package/dist/default_templates/patterns/safety/code-safety-rules.md +0 -17
- package/dist/default_templates/patterns/safety/credential-handling.md +0 -17
- package/dist/default_templates/patterns/safety/destructive-warnings.md +0 -17
- package/dist/default_templates/patterns/safety/refusal-messages.md +0 -17
- package/dist/default_templates/patterns/tone/adaptive-tone.md +0 -17
- package/dist/default_templates/patterns/tone/concise-communication.md +0 -17
- package/dist/default_templates/patterns/tone/forbidden-phrases.md +0 -17
- package/dist/default_templates/patterns/tool-guidelines/function-schemas.md +0 -143
- package/dist/default_templates/patterns/tool-guidelines/parameter-examples.md +0 -137
- package/dist/default_templates/patterns/tool-guidelines/usage-policies.md +0 -105
- package/dist/default_templates/php/framework-laravel.md +0 -112
- package/dist/default_templates/php/lang-php.md +0 -94
- package/dist/default_templates/python/lang-python.md +0 -508
- package/dist/default_templates/react/react-core.md +0 -677
- package/dist/default_templates/react/react-zustand.md +0 -7
- package/dist/default_templates/ruby/framework-rails.md +0 -309
- package/dist/default_templates/ruby/framework-sinatra.md +0 -227
- package/dist/default_templates/ruby/lang-ruby.md +0 -216
- package/dist/default_templates/rust/lang-rust.md +0 -89
- package/dist/default_templates/swift/framework-vapor.md +0 -352
- package/dist/default_templates/swift/lang-swift.md +0 -291
- package/dist/default_templates/vue/style-primevue.md +0 -6
- package/dist/default_templates/vue/style-quasar.md +0 -22
- package/dist/default_templates/vue/vue-core.md +0 -108
- package/dist/default_templates/vue/vue-pinia.md +0 -5
- package/dist/index-0GK4RlUx.js +0 -1748
- package/dist/index-0GK4RlUx.js.map +0 -1
- package/dist/index-AkVwRl-r.js +0 -92
- package/dist/index-AkVwRl-r.js.map +0 -1
- package/dist/index-B6BeG1yT.cjs +0 -68
- package/dist/index-B6BeG1yT.cjs.map +0 -1
- package/dist/index-B8pyjKdF.js +0 -94
- package/dist/index-B8pyjKdF.js.map +0 -1
- package/dist/index-BQ6v041y.js +0 -13
- package/dist/index-BQ6v041y.js.map +0 -1
- package/dist/index-Baxb1vI_.js +0 -210
- package/dist/index-Baxb1vI_.js.map +0 -1
- package/dist/index-Bg8DD8ku.js +0 -216
- package/dist/index-Bg8DD8ku.js.map +0 -1
- package/dist/index-BqTqxCpG.cjs +0 -89
- package/dist/index-BqTqxCpG.cjs.map +0 -1
- package/dist/index-Bv4Q1Pr7.cjs +0 -33
- package/dist/index-Bv4Q1Pr7.cjs.map +0 -1
- package/dist/index-CN8J45Nc.cjs +0 -24
- package/dist/index-CN8J45Nc.cjs.map +0 -1
- package/dist/index-CPbv2Od1.js +0 -62
- package/dist/index-CPbv2Od1.js.map +0 -1
- package/dist/index-Cf-MC6Al.js +0 -63
- package/dist/index-Cf-MC6Al.js.map +0 -1
- package/dist/index-DDPXXXDy.cjs +0 -19
- package/dist/index-DDPXXXDy.cjs.map +0 -1
- package/dist/index-DO30AzDe.cjs +0 -19
- package/dist/index-DO30AzDe.cjs.map +0 -1
- package/dist/index-DkvW5yBY.js +0 -2249
- package/dist/index-DkvW5yBY.js.map +0 -1
- package/dist/index-Dn1ehjIj.cjs +0 -80
- package/dist/index-Dn1ehjIj.cjs.map +0 -1
- package/dist/index-DqHvgoXJ.cjs +0 -19
- package/dist/index-DqHvgoXJ.cjs.map +0 -1
- package/dist/index-K39pdw94.cjs +0 -31
- package/dist/index-K39pdw94.cjs.map +0 -1
- package/dist/index-OT2XAJkc.js +0 -117
- package/dist/index-OT2XAJkc.js.map +0 -1
- package/dist/index-TPAX4XKg.cjs +0 -30
- package/dist/index-TPAX4XKg.cjs.map +0 -1
- package/dist/index-WmVSB57y.js +0 -107
- package/dist/index-WmVSB57y.js.map +0 -1
- package/dist/index-mYXvc3Fs.js +0 -68
- package/dist/index-mYXvc3Fs.js.map +0 -1
- package/dist/index-nioXOg4m.cjs +0 -76
- package/dist/index-nioXOg4m.cjs.map +0 -1
- package/dist/index-okhY3fWD.cjs +0 -2
- package/dist/index-okhY3fWD.cjs.map +0 -1
|
@@ -1,621 +0,0 @@
|
|
|
1
|
-
# Domain-Driven Design (DDD) Patterns
|
|
2
|
-
|
|
3
|
-
## Purpose
|
|
4
|
-
Model complex business domains through strategic and tactical patterns. Align software structure with business concepts. Manage complexity through bounded contexts and rich domain models.
|
|
5
|
-
|
|
6
|
-
## Core Concepts
|
|
7
|
-
|
|
8
|
-
### Ubiquitous Language
|
|
9
|
-
|
|
10
|
-
**The same terms used by domain experts must be used in code.**
|
|
11
|
-
|
|
12
|
-
```typescript
|
|
13
|
-
// ✅ Good: Uses business domain language
|
|
14
|
-
class Order {
|
|
15
|
-
confirmPayment(payment: Payment): void {
|
|
16
|
-
if (this.status !== OrderStatus.AwaitingPayment) {
|
|
17
|
-
throw new InvalidOrderStateError('Order is not awaiting payment');
|
|
18
|
-
}
|
|
19
|
-
this.status = OrderStatus.Confirmed;
|
|
20
|
-
this.events.push(new OrderConfirmedEvent(this.id));
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ❌ Bad: Technical jargon, not business language
|
|
25
|
-
class Order {
|
|
26
|
-
updateStatus(newStatus: number): void {
|
|
27
|
-
this.statusCode = newStatus;
|
|
28
|
-
this.lastModified = new Date();
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
**Learn the language from domain experts, then use it everywhere:** code, tests, docs, discussions.
|
|
34
|
-
|
|
35
|
-
## Building Blocks (Tactical Patterns)
|
|
36
|
-
|
|
37
|
-
### 1. Entities
|
|
38
|
-
|
|
39
|
-
Objects with identity that persist over time. Two entities are equal if they have the same ID, even if their attributes differ.
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
// ✅ Entity: Has identity (id)
|
|
43
|
-
export class User {
|
|
44
|
-
constructor(
|
|
45
|
-
private readonly id: UserId, // Identity (value object)
|
|
46
|
-
private email: Email, // Value object
|
|
47
|
-
private name: string,
|
|
48
|
-
private createdAt: Date
|
|
49
|
-
) {}
|
|
50
|
-
|
|
51
|
-
getId(): UserId {
|
|
52
|
-
return this.id;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
changeEmail(newEmail: Email): void {
|
|
56
|
-
if (!newEmail.isValid()) {
|
|
57
|
-
throw new InvalidEmailError();
|
|
58
|
-
}
|
|
59
|
-
this.email = newEmail;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Equality based on identity
|
|
63
|
-
equals(other: User): boolean {
|
|
64
|
-
return this.id.equals(other.id);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Usage
|
|
69
|
-
const user1 = new User(UserId.create(), Email.create('john@example.com'), 'John', new Date());
|
|
70
|
-
const user2 = new User(UserId.create(), Email.create('jane@example.com'), 'Jane', new Date());
|
|
71
|
-
|
|
72
|
-
user1.equals(user2); // false (different IDs)
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
**Rules:**
|
|
76
|
-
- Has a unique identifier (ID)
|
|
77
|
-
- Equality based on ID, not attributes
|
|
78
|
-
- Contains business logic methods
|
|
79
|
-
- Can change over time while keeping identity
|
|
80
|
-
|
|
81
|
-
### 2. Value Objects
|
|
82
|
-
|
|
83
|
-
Immutable objects defined by their attributes. Two value objects are equal if all their attributes are equal.
|
|
84
|
-
|
|
85
|
-
```typescript
|
|
86
|
-
// ✅ Value Object: Immutable, equality by attributes
|
|
87
|
-
export class Money {
|
|
88
|
-
constructor(
|
|
89
|
-
private readonly amount: number,
|
|
90
|
-
private readonly currency: string
|
|
91
|
-
) {
|
|
92
|
-
if (amount < 0) {
|
|
93
|
-
throw new NegativeAmountError();
|
|
94
|
-
}
|
|
95
|
-
Object.freeze(this); // Enforce immutability
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
add(other: Money): Money {
|
|
99
|
-
if (this.currency !== other.currency) {
|
|
100
|
-
throw new CurrencyMismatchError();
|
|
101
|
-
}
|
|
102
|
-
return new Money(this.amount + other.amount, this.currency);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
multiply(factor: number): Money {
|
|
106
|
-
return new Money(this.amount * factor, this.currency);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
equals(other: Money): boolean {
|
|
110
|
-
return this.amount === other.amount && this.currency === other.currency;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
toString(): string {
|
|
114
|
-
return `${this.amount} ${this.currency}`;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Usage
|
|
119
|
-
const price = new Money(100, 'USD');
|
|
120
|
-
const tax = price.multiply(0.2);
|
|
121
|
-
const total = price.add(tax); // New object, original unchanged
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
**Examples:** Email, Address, Money, DateRange, PhoneNumber
|
|
125
|
-
|
|
126
|
-
**Rules:**
|
|
127
|
-
- **ALWAYS** immutable (no setters)
|
|
128
|
-
- Equality based on all attributes
|
|
129
|
-
- No identity (no ID)
|
|
130
|
-
- Encapsulate validation
|
|
131
|
-
- Return new instances for changes
|
|
132
|
-
|
|
133
|
-
### 3. Aggregates
|
|
134
|
-
|
|
135
|
-
Cluster of entities and value objects with a single root entity. The root controls access and enforces invariants.
|
|
136
|
-
|
|
137
|
-
```typescript
|
|
138
|
-
// ✅ Aggregate Root: Order
|
|
139
|
-
export class Order {
|
|
140
|
-
private readonly id: OrderId;
|
|
141
|
-
private customerId: CustomerId;
|
|
142
|
-
private items: OrderItem[] = []; // Entities within aggregate
|
|
143
|
-
private status: OrderStatus;
|
|
144
|
-
private total: Money;
|
|
145
|
-
|
|
146
|
-
constructor(customerId: CustomerId) {
|
|
147
|
-
this.id = OrderId.create();
|
|
148
|
-
this.customerId = customerId;
|
|
149
|
-
this.status = OrderStatus.Draft;
|
|
150
|
-
this.total = Money.zero('USD');
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ✅ All modifications go through aggregate root
|
|
154
|
-
addItem(product: Product, quantity: number): void {
|
|
155
|
-
if (this.status !== OrderStatus.Draft) {
|
|
156
|
-
throw new OrderAlreadyConfirmedError();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const item = new OrderItem(product, quantity);
|
|
160
|
-
this.items.push(item);
|
|
161
|
-
this.recalculateTotal(); // Maintain invariant
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
removeItem(productId: ProductId): void {
|
|
165
|
-
if (this.status !== OrderStatus.Draft) {
|
|
166
|
-
throw new OrderAlreadyConfirmedError();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
this.items = this.items.filter(item => !item.productId.equals(productId));
|
|
170
|
-
this.recalculateTotal();
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
confirm(): void {
|
|
174
|
-
if (this.items.length === 0) {
|
|
175
|
-
throw new EmptyOrderError();
|
|
176
|
-
}
|
|
177
|
-
this.status = OrderStatus.Confirmed;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ✅ Enforce invariant: total = sum of items
|
|
181
|
-
private recalculateTotal(): void {
|
|
182
|
-
this.total = this.items.reduce(
|
|
183
|
-
(sum, item) => sum.add(item.getTotal()),
|
|
184
|
-
Money.zero('USD')
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
getTotal(): Money {
|
|
189
|
-
return this.total;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ❌ Never modify entities within aggregate from outside
|
|
194
|
-
class OrderItem {
|
|
195
|
-
constructor(
|
|
196
|
-
private product: Product,
|
|
197
|
-
private quantity: number
|
|
198
|
-
) {}
|
|
199
|
-
|
|
200
|
-
getTotal(): Money {
|
|
201
|
-
return this.product.price.multiply(this.quantity);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// ❌ Bad: Bypassing aggregate root
|
|
206
|
-
order.items[0].quantity = 10; // Violates encapsulation!
|
|
207
|
-
|
|
208
|
-
// ✅ Good: Use aggregate root
|
|
209
|
-
order.addItem(product, 10);
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
**Aggregate Boundaries:**
|
|
213
|
-
- Small aggregates (1-3 entities) perform better
|
|
214
|
-
- One aggregate per transaction
|
|
215
|
-
- Reference other aggregates by ID, not direct reference
|
|
216
|
-
|
|
217
|
-
```typescript
|
|
218
|
-
// ✅ Good: Reference by ID
|
|
219
|
-
class Order {
|
|
220
|
-
private customerId: CustomerId; // ID reference
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ❌ Bad: Direct reference across aggregates
|
|
224
|
-
class Order {
|
|
225
|
-
private customer: Customer; // Couples two aggregates
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
### 4. Domain Events
|
|
230
|
-
|
|
231
|
-
Represent something significant that happened in the domain.
|
|
232
|
-
|
|
233
|
-
```typescript
|
|
234
|
-
// ✅ Domain Event: Immutable record of what happened
|
|
235
|
-
export class OrderConfirmedEvent {
|
|
236
|
-
constructor(
|
|
237
|
-
public readonly orderId: OrderId,
|
|
238
|
-
public readonly customerId: CustomerId,
|
|
239
|
-
public readonly total: Money,
|
|
240
|
-
public readonly occurredAt: Date = new Date()
|
|
241
|
-
) {
|
|
242
|
-
Object.freeze(this);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// ✅ Aggregate raises events
|
|
247
|
-
export class Order {
|
|
248
|
-
private events: DomainEvent[] = [];
|
|
249
|
-
|
|
250
|
-
confirm(): void {
|
|
251
|
-
if (this.items.length === 0) {
|
|
252
|
-
throw new EmptyOrderError();
|
|
253
|
-
}
|
|
254
|
-
this.status = OrderStatus.Confirmed;
|
|
255
|
-
|
|
256
|
-
// Raise event
|
|
257
|
-
this.events.push(
|
|
258
|
-
new OrderConfirmedEvent(this.id, this.customerId, this.total)
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
getEvents(): DomainEvent[] {
|
|
263
|
-
return [...this.events];
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
clearEvents(): void {
|
|
267
|
-
this.events = [];
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// ✅ Event handler in separate bounded context
|
|
272
|
-
class InventoryEventHandler {
|
|
273
|
-
async onOrderConfirmed(event: OrderConfirmedEvent): Promise<void> {
|
|
274
|
-
// Reserve inventory when order confirmed
|
|
275
|
-
await this.inventoryService.reserveItems(event.orderId);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
**Use events for:**
|
|
281
|
-
- Cross-aggregate consistency (eventual consistency)
|
|
282
|
-
- Decoupling bounded contexts
|
|
283
|
-
- Audit trail
|
|
284
|
-
- Triggering side effects
|
|
285
|
-
|
|
286
|
-
### 5. Domain Services
|
|
287
|
-
|
|
288
|
-
Business logic that doesn't belong to a single entity or value object.
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
// ✅ Domain Service: Stateless, operates on domain objects
|
|
292
|
-
export class OrderPricingService {
|
|
293
|
-
calculateDiscount(order: Order, customer: Customer): Money {
|
|
294
|
-
let discount = Money.zero('USD');
|
|
295
|
-
|
|
296
|
-
// VIP customers get 10% off
|
|
297
|
-
if (customer.isVip()) {
|
|
298
|
-
discount = order.getTotal().multiply(0.1);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Bulk orders (>10 items) get additional 5%
|
|
302
|
-
if (order.getItemCount() > 10) {
|
|
303
|
-
const bulkDiscount = order.getTotal().multiply(0.05);
|
|
304
|
-
discount = discount.add(bulkDiscount);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return discount;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// ❌ Bad: Logic scattered across entities
|
|
312
|
-
class Order {
|
|
313
|
-
calculateVipDiscount(): Money { /* ... */ }
|
|
314
|
-
calculateBulkDiscount(): Money { /* ... */ }
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
class Customer {
|
|
318
|
-
calculateOrderDiscount(order: Order): Money { /* ... */ }
|
|
319
|
-
}
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
**When to use domain services:**
|
|
323
|
-
- Logic involves multiple aggregates
|
|
324
|
-
- Logic doesn't naturally fit in an entity
|
|
325
|
-
- Stateless operations
|
|
326
|
-
|
|
327
|
-
### 6. Repositories
|
|
328
|
-
|
|
329
|
-
Provide collection-like interface for aggregates. Only for aggregate roots.
|
|
330
|
-
|
|
331
|
-
```typescript
|
|
332
|
-
// ✅ Repository: Collection interface for aggregates
|
|
333
|
-
export interface IOrderRepository {
|
|
334
|
-
save(order: Order): Promise<void>;
|
|
335
|
-
findById(id: OrderId): Promise<Order | null>;
|
|
336
|
-
findByCustomer(customerId: CustomerId): Promise<Order[]>;
|
|
337
|
-
delete(id: OrderId): Promise<void>;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// ✅ Implementation in infrastructure layer
|
|
341
|
-
export class PostgresOrderRepository implements IOrderRepository {
|
|
342
|
-
constructor(private db: Database) {}
|
|
343
|
-
|
|
344
|
-
async save(order: Order): Promise<void> {
|
|
345
|
-
await this.db.transaction(async (tx) => {
|
|
346
|
-
// Save aggregate root
|
|
347
|
-
await tx.query(
|
|
348
|
-
'INSERT INTO orders (id, customer_id, status, total) VALUES ($1, $2, $3, $4)',
|
|
349
|
-
[order.id, order.customerId, order.status, order.total]
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
// Save child entities (items)
|
|
353
|
-
for (const item of order.items) {
|
|
354
|
-
await tx.query(
|
|
355
|
-
'INSERT INTO order_items (order_id, product_id, quantity) VALUES ($1, $2, $3)',
|
|
356
|
-
[order.id, item.productId, item.quantity]
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Publish domain events
|
|
361
|
-
for (const event of order.getEvents()) {
|
|
362
|
-
await this.eventPublisher.publish(event);
|
|
363
|
-
}
|
|
364
|
-
order.clearEvents();
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
async findById(id: OrderId): Promise<Order | null> {
|
|
369
|
-
const row = await this.db.query('SELECT * FROM orders WHERE id = $1', [id.value]);
|
|
370
|
-
if (!row) return null;
|
|
371
|
-
|
|
372
|
-
const items = await this.db.query('SELECT * FROM order_items WHERE order_id = $1', [id.value]);
|
|
373
|
-
|
|
374
|
-
return this.mapToAggregate(row, items);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
**Rules:**
|
|
380
|
-
- One repository per aggregate root
|
|
381
|
-
- No repositories for entities inside aggregates
|
|
382
|
-
- Interface in domain layer, implementation in infrastructure
|
|
383
|
-
|
|
384
|
-
## Strategic Patterns
|
|
385
|
-
|
|
386
|
-
### Bounded Contexts
|
|
387
|
-
|
|
388
|
-
Explicit boundaries where a domain model applies. Same term can have different meanings in different contexts.
|
|
389
|
-
|
|
390
|
-
```
|
|
391
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
392
|
-
│ E-commerce System │
|
|
393
|
-
├─────────────────┬─────────────────┬────────────────────────┤
|
|
394
|
-
│ Sales Context │ Inventory Ctx │ Shipping Context │
|
|
395
|
-
├─────────────────┼─────────────────┼────────────────────────┤
|
|
396
|
-
│ Product: │ Product: │ Package: │
|
|
397
|
-
│ - price │ - quantity │ - weight │
|
|
398
|
-
│ - description │ - location │ - dimensions │
|
|
399
|
-
│ - reviews │ - reorderLevel │ - tracking number │
|
|
400
|
-
│ │ │ │
|
|
401
|
-
│ Order: │ │ Shipment: │
|
|
402
|
-
│ - customer │ │ - destination │
|
|
403
|
-
│ - total │ │ - carrier │
|
|
404
|
-
│ - items │ │ - status │
|
|
405
|
-
└─────────────────┴─────────────────┴────────────────────────┘
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
**Example: Product in different contexts**
|
|
409
|
-
|
|
410
|
-
```typescript
|
|
411
|
-
// Sales Context - Product focuses on sellability
|
|
412
|
-
namespace SalesContext {
|
|
413
|
-
export class Product {
|
|
414
|
-
constructor(
|
|
415
|
-
public id: ProductId,
|
|
416
|
-
public name: string,
|
|
417
|
-
public price: Money,
|
|
418
|
-
public description: string
|
|
419
|
-
) {}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Inventory Context - Product focuses on stock management
|
|
424
|
-
namespace InventoryContext {
|
|
425
|
-
export class Product {
|
|
426
|
-
constructor(
|
|
427
|
-
public id: ProductId,
|
|
428
|
-
public sku: string,
|
|
429
|
-
public quantityOnHand: number,
|
|
430
|
-
public reorderLevel: number,
|
|
431
|
-
public warehouseLocation: string
|
|
432
|
-
) {}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
### Context Mapping
|
|
438
|
-
|
|
439
|
-
Define relationships between bounded contexts.
|
|
440
|
-
|
|
441
|
-
```typescript
|
|
442
|
-
// ✅ Anti-Corruption Layer (ACL): Translate between contexts
|
|
443
|
-
export class SalesInventoryAdapter {
|
|
444
|
-
constructor(private inventoryService: InventoryService) {}
|
|
445
|
-
|
|
446
|
-
async checkAvailability(salesProduct: SalesContext.Product): Promise<boolean> {
|
|
447
|
-
// Translate Sales Product → Inventory Product
|
|
448
|
-
const inventoryProduct = await this.inventoryService.findBySku(
|
|
449
|
-
salesProduct.id.toSku()
|
|
450
|
-
);
|
|
451
|
-
|
|
452
|
-
return inventoryProduct.quantityOnHand > 0;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
## Layered Architecture with DDD
|
|
458
|
-
|
|
459
|
-
```
|
|
460
|
-
┌──────────────────────────────────────────────┐
|
|
461
|
-
│ Presentation Layer (Controllers, UI) │
|
|
462
|
-
│ - Handles HTTP requests │
|
|
463
|
-
│ - Maps DTOs ↔ Domain models │
|
|
464
|
-
└──────────────────────────────────────────────┘
|
|
465
|
-
↓
|
|
466
|
-
┌──────────────────────────────────────────────┐
|
|
467
|
-
│ Application Layer (Use Cases, Services) │
|
|
468
|
-
│ - Orchestrates domain objects │
|
|
469
|
-
│ - Transaction boundaries │
|
|
470
|
-
│ - Triggers domain events │
|
|
471
|
-
└──────────────────────────────────────────────┘
|
|
472
|
-
↓
|
|
473
|
-
┌──────────────────────────────────────────────┐
|
|
474
|
-
│ Domain Layer (Entities, VOs, Aggregates) │
|
|
475
|
-
│ - Business logic │
|
|
476
|
-
│ - Domain rules and invariants │
|
|
477
|
-
│ - Rich domain model │
|
|
478
|
-
└──────────────────────────────────────────────┘
|
|
479
|
-
↓
|
|
480
|
-
┌──────────────────────────────────────────────┐
|
|
481
|
-
│ Infrastructure Layer (DB, APIs, Messaging) │
|
|
482
|
-
│ - Repository implementations │
|
|
483
|
-
│ - External service clients │
|
|
484
|
-
│ - Database access │
|
|
485
|
-
└──────────────────────────────────────────────┘
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
**Example: Complete DDD Flow**
|
|
489
|
-
|
|
490
|
-
```typescript
|
|
491
|
-
// 1. Presentation Layer: Controller
|
|
492
|
-
export class OrderController {
|
|
493
|
-
constructor(private createOrderUseCase: CreateOrderUseCase) {}
|
|
494
|
-
|
|
495
|
-
async createOrder(req: Request, res: Response) {
|
|
496
|
-
const dto: CreateOrderDTO = req.body;
|
|
497
|
-
|
|
498
|
-
const result = await this.createOrderUseCase.execute({
|
|
499
|
-
customerId: dto.customerId,
|
|
500
|
-
items: dto.items,
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
if (result.isFailure) {
|
|
504
|
-
return res.status(400).json({ error: result.error });
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
return res.status(201).json({ orderId: result.value.id });
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// 2. Application Layer: Use Case
|
|
512
|
-
export class CreateOrderUseCase {
|
|
513
|
-
constructor(
|
|
514
|
-
private orderRepo: IOrderRepository,
|
|
515
|
-
private productRepo: IProductRepository,
|
|
516
|
-
private pricingService: OrderPricingService
|
|
517
|
-
) {}
|
|
518
|
-
|
|
519
|
-
async execute(request: CreateOrderRequest): Promise<Result<Order>> {
|
|
520
|
-
// Load customer aggregate
|
|
521
|
-
const customer = await this.customerRepo.findById(request.customerId);
|
|
522
|
-
if (!customer) {
|
|
523
|
-
return Result.fail('Customer not found');
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Create order aggregate
|
|
527
|
-
const order = new Order(request.customerId);
|
|
528
|
-
|
|
529
|
-
// Add items
|
|
530
|
-
for (const item of request.items) {
|
|
531
|
-
const product = await this.productRepo.findById(item.productId);
|
|
532
|
-
if (!product) {
|
|
533
|
-
return Result.fail(`Product ${item.productId} not found`);
|
|
534
|
-
}
|
|
535
|
-
order.addItem(product, item.quantity);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Apply business logic via domain service
|
|
539
|
-
const discount = this.pricingService.calculateDiscount(order, customer);
|
|
540
|
-
order.applyDiscount(discount);
|
|
541
|
-
|
|
542
|
-
// Confirm order
|
|
543
|
-
order.confirm();
|
|
544
|
-
|
|
545
|
-
// Persist aggregate
|
|
546
|
-
await this.orderRepo.save(order);
|
|
547
|
-
|
|
548
|
-
return Result.ok(order);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// 3. Domain Layer: Aggregate
|
|
553
|
-
export class Order {
|
|
554
|
-
// See aggregate example above
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// 4. Infrastructure Layer: Repository
|
|
558
|
-
export class PostgresOrderRepository implements IOrderRepository {
|
|
559
|
-
// See repository example above
|
|
560
|
-
}
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
## File Organization
|
|
564
|
-
|
|
565
|
-
```
|
|
566
|
-
src/
|
|
567
|
-
├── presentation/ # Presentation layer
|
|
568
|
-
│ └── controllers/
|
|
569
|
-
│ └── OrderController.ts
|
|
570
|
-
│
|
|
571
|
-
├── application/ # Application layer
|
|
572
|
-
│ └── use-cases/
|
|
573
|
-
│ └── CreateOrderUseCase.ts
|
|
574
|
-
│
|
|
575
|
-
├── domain/ # Domain layer
|
|
576
|
-
│ ├── order/ # Bounded context
|
|
577
|
-
│ │ ├── entities/
|
|
578
|
-
│ │ │ ├── Order.ts
|
|
579
|
-
│ │ │ └── OrderItem.ts
|
|
580
|
-
│ │ ├── value-objects/
|
|
581
|
-
│ │ │ ├── OrderId.ts
|
|
582
|
-
│ │ │ └── OrderStatus.ts
|
|
583
|
-
│ │ ├── repositories/
|
|
584
|
-
│ │ │ └── IOrderRepository.ts
|
|
585
|
-
│ │ ├── services/
|
|
586
|
-
│ │ │ └── OrderPricingService.ts
|
|
587
|
-
│ │ └── events/
|
|
588
|
-
│ │ └── OrderConfirmedEvent.ts
|
|
589
|
-
│ │
|
|
590
|
-
│ ├── customer/ # Another bounded context
|
|
591
|
-
│ │ ├── entities/
|
|
592
|
-
│ │ │ └── Customer.ts
|
|
593
|
-
│ │ └── value-objects/
|
|
594
|
-
│ │ └── CustomerId.ts
|
|
595
|
-
│ │
|
|
596
|
-
│ └── shared/ # Shared kernel
|
|
597
|
-
│ └── value-objects/
|
|
598
|
-
│ └── Money.ts
|
|
599
|
-
│
|
|
600
|
-
└── infrastructure/ # Infrastructure layer
|
|
601
|
-
├── repositories/
|
|
602
|
-
│ └── PostgresOrderRepository.ts
|
|
603
|
-
└── messaging/
|
|
604
|
-
└── RabbitMQEventPublisher.ts
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
## Rules Summary
|
|
608
|
-
|
|
609
|
-
- **ALWAYS** use ubiquitous language from domain experts
|
|
610
|
-
- **ALWAYS** model aggregates with clear boundaries
|
|
611
|
-
- **ALWAYS** make value objects immutable
|
|
612
|
-
- **ALWAYS** enforce invariants in aggregate roots
|
|
613
|
-
- **ALWAYS** access child entities through aggregate root
|
|
614
|
-
- **ALWAYS** reference other aggregates by ID, not direct reference
|
|
615
|
-
- **NEVER** allow direct modification of entities inside aggregates
|
|
616
|
-
- **NEVER** create repositories for non-root entities
|
|
617
|
-
- **NEVER** let infrastructure concerns leak into domain layer
|
|
618
|
-
- **PREFER** small aggregates (1-3 entities)
|
|
619
|
-
- **PREFER** eventual consistency between aggregates
|
|
620
|
-
- **CONSIDER** domain events for cross-aggregate operations
|
|
621
|
-
- **CONSIDER** anti-corruption layers between bounded contexts
|