opencode-metis 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -0
- package/dist/cli.cjs +63 -0
- package/dist/mcp-server.cjs +51 -0
- package/dist/plugin.cjs +4 -0
- package/dist/worker.cjs +224 -0
- package/opencode/agent/the-analyst/feature-prioritization.md +66 -0
- package/opencode/agent/the-analyst/market-research.md +77 -0
- package/opencode/agent/the-analyst/project-coordination.md +81 -0
- package/opencode/agent/the-analyst/requirements-analysis.md +77 -0
- package/opencode/agent/the-architect/compatibility-review.md +138 -0
- package/opencode/agent/the-architect/complexity-review.md +137 -0
- package/opencode/agent/the-architect/quality-review.md +67 -0
- package/opencode/agent/the-architect/security-review.md +127 -0
- package/opencode/agent/the-architect/system-architecture.md +119 -0
- package/opencode/agent/the-architect/system-documentation.md +83 -0
- package/opencode/agent/the-architect/technology-research.md +85 -0
- package/opencode/agent/the-chief.md +79 -0
- package/opencode/agent/the-designer/accessibility-implementation.md +101 -0
- package/opencode/agent/the-designer/design-foundation.md +74 -0
- package/opencode/agent/the-designer/interaction-architecture.md +75 -0
- package/opencode/agent/the-designer/user-research.md +70 -0
- package/opencode/agent/the-meta-agent.md +155 -0
- package/opencode/agent/the-platform-engineer/ci-cd-pipelines.md +109 -0
- package/opencode/agent/the-platform-engineer/containerization.md +106 -0
- package/opencode/agent/the-platform-engineer/data-architecture.md +81 -0
- package/opencode/agent/the-platform-engineer/dependency-review.md +144 -0
- package/opencode/agent/the-platform-engineer/deployment-automation.md +81 -0
- package/opencode/agent/the-platform-engineer/infrastructure-as-code.md +107 -0
- package/opencode/agent/the-platform-engineer/performance-tuning.md +82 -0
- package/opencode/agent/the-platform-engineer/pipeline-engineering.md +81 -0
- package/opencode/agent/the-platform-engineer/production-monitoring.md +105 -0
- package/opencode/agent/the-qa-engineer/exploratory-testing.md +66 -0
- package/opencode/agent/the-qa-engineer/performance-testing.md +81 -0
- package/opencode/agent/the-qa-engineer/quality-assurance.md +77 -0
- package/opencode/agent/the-qa-engineer/test-execution.md +66 -0
- package/opencode/agent/the-software-engineer/api-development.md +78 -0
- package/opencode/agent/the-software-engineer/component-development.md +79 -0
- package/opencode/agent/the-software-engineer/concurrency-review.md +141 -0
- package/opencode/agent/the-software-engineer/domain-modeling.md +66 -0
- package/opencode/agent/the-software-engineer/performance-optimization.md +113 -0
- package/opencode/command/analyze.md +149 -0
- package/opencode/command/constitution.md +178 -0
- package/opencode/command/debug.md +194 -0
- package/opencode/command/document.md +178 -0
- package/opencode/command/implement.md +225 -0
- package/opencode/command/refactor.md +207 -0
- package/opencode/command/review.md +229 -0
- package/opencode/command/simplify.md +267 -0
- package/opencode/command/specify.md +191 -0
- package/opencode/command/validate.md +224 -0
- package/opencode/skill/accessibility-design/SKILL.md +566 -0
- package/opencode/skill/accessibility-design/checklists/wcag-checklist.md +435 -0
- package/opencode/skill/agent-coordination/SKILL.md +224 -0
- package/opencode/skill/api-contract-design/SKILL.md +550 -0
- package/opencode/skill/api-contract-design/templates/graphql-schema-template.md +818 -0
- package/opencode/skill/api-contract-design/templates/rest-api-template.md +417 -0
- package/opencode/skill/architecture-design/SKILL.md +160 -0
- package/opencode/skill/architecture-design/examples/architecture-examples.md +170 -0
- package/opencode/skill/architecture-design/template.md +749 -0
- package/opencode/skill/architecture-design/validation.md +99 -0
- package/opencode/skill/architecture-selection/SKILL.md +522 -0
- package/opencode/skill/architecture-selection/examples/adrs/001-example-adr.md +71 -0
- package/opencode/skill/architecture-selection/examples/architecture-patterns.md +239 -0
- package/opencode/skill/bug-diagnosis/SKILL.md +235 -0
- package/opencode/skill/code-quality-review/SKILL.md +337 -0
- package/opencode/skill/code-quality-review/examples/anti-patterns.md +629 -0
- package/opencode/skill/code-quality-review/reference.md +322 -0
- package/opencode/skill/code-review/SKILL.md +363 -0
- package/opencode/skill/code-review/reference.md +450 -0
- package/opencode/skill/codebase-analysis/SKILL.md +139 -0
- package/opencode/skill/codebase-navigation/SKILL.md +227 -0
- package/opencode/skill/codebase-navigation/examples/exploration-patterns.md +263 -0
- package/opencode/skill/coding-conventions/SKILL.md +178 -0
- package/opencode/skill/coding-conventions/checklists/accessibility-checklist.md +176 -0
- package/opencode/skill/coding-conventions/checklists/performance-checklist.md +154 -0
- package/opencode/skill/coding-conventions/checklists/security-checklist.md +127 -0
- package/opencode/skill/constitution-validation/SKILL.md +315 -0
- package/opencode/skill/constitution-validation/examples/CONSTITUTION.md +202 -0
- package/opencode/skill/constitution-validation/reference/rule-patterns.md +328 -0
- package/opencode/skill/constitution-validation/template.md +115 -0
- package/opencode/skill/context-preservation/SKILL.md +445 -0
- package/opencode/skill/data-modeling/SKILL.md +385 -0
- package/opencode/skill/data-modeling/templates/schema-design-template.md +268 -0
- package/opencode/skill/deployment-pipeline-design/SKILL.md +579 -0
- package/opencode/skill/deployment-pipeline-design/templates/pipeline-template.md +633 -0
- package/opencode/skill/documentation-extraction/SKILL.md +259 -0
- package/opencode/skill/documentation-sync/SKILL.md +431 -0
- package/opencode/skill/domain-driven-design/SKILL.md +509 -0
- package/opencode/skill/domain-driven-design/examples/ddd-patterns.md +688 -0
- package/opencode/skill/domain-driven-design/reference.md +465 -0
- package/opencode/skill/drift-detection/SKILL.md +383 -0
- package/opencode/skill/drift-detection/reference.md +340 -0
- package/opencode/skill/error-recovery/SKILL.md +162 -0
- package/opencode/skill/error-recovery/examples/error-patterns.md +484 -0
- package/opencode/skill/feature-prioritization/SKILL.md +419 -0
- package/opencode/skill/feature-prioritization/examples/rice-template.md +139 -0
- package/opencode/skill/feature-prioritization/reference.md +256 -0
- package/opencode/skill/git-workflow/SKILL.md +453 -0
- package/opencode/skill/implementation-planning/SKILL.md +215 -0
- package/opencode/skill/implementation-planning/examples/phase-examples.md +217 -0
- package/opencode/skill/implementation-planning/template.md +220 -0
- package/opencode/skill/implementation-planning/validation.md +88 -0
- package/opencode/skill/implementation-verification/SKILL.md +272 -0
- package/opencode/skill/knowledge-capture/SKILL.md +265 -0
- package/opencode/skill/knowledge-capture/reference/knowledge-capture.md +402 -0
- package/opencode/skill/knowledge-capture/reference.md +444 -0
- package/opencode/skill/knowledge-capture/templates/domain-template.md +325 -0
- package/opencode/skill/knowledge-capture/templates/interface-template.md +255 -0
- package/opencode/skill/knowledge-capture/templates/pattern-template.md +144 -0
- package/opencode/skill/observability-design/SKILL.md +291 -0
- package/opencode/skill/observability-design/references/monitoring-patterns.md +461 -0
- package/opencode/skill/pattern-detection/SKILL.md +171 -0
- package/opencode/skill/pattern-detection/examples/common-patterns.md +359 -0
- package/opencode/skill/performance-analysis/SKILL.md +266 -0
- package/opencode/skill/performance-analysis/references/profiling-tools.md +499 -0
- package/opencode/skill/requirements-analysis/SKILL.md +139 -0
- package/opencode/skill/requirements-analysis/examples/good-prd.md +66 -0
- package/opencode/skill/requirements-analysis/template.md +177 -0
- package/opencode/skill/requirements-analysis/validation.md +69 -0
- package/opencode/skill/requirements-elicitation/SKILL.md +518 -0
- package/opencode/skill/requirements-elicitation/examples/interview-questions.md +226 -0
- package/opencode/skill/requirements-elicitation/examples/user-stories.md +414 -0
- package/opencode/skill/safe-refactoring/SKILL.md +312 -0
- package/opencode/skill/safe-refactoring/reference/code-smells.md +347 -0
- package/opencode/skill/security-assessment/SKILL.md +421 -0
- package/opencode/skill/security-assessment/checklists/security-review-checklist.md +285 -0
- package/opencode/skill/specification-management/SKILL.md +143 -0
- package/opencode/skill/specification-management/readme-template.md +32 -0
- package/opencode/skill/specification-management/reference.md +115 -0
- package/opencode/skill/specification-management/spec.py +229 -0
- package/opencode/skill/specification-validation/SKILL.md +397 -0
- package/opencode/skill/specification-validation/reference/3cs-framework.md +306 -0
- package/opencode/skill/specification-validation/reference/ambiguity-detection.md +132 -0
- package/opencode/skill/specification-validation/reference/constitution-validation.md +301 -0
- package/opencode/skill/specification-validation/reference/drift-detection.md +383 -0
- package/opencode/skill/task-delegation/SKILL.md +607 -0
- package/opencode/skill/task-delegation/examples/file-coordination.md +495 -0
- package/opencode/skill/task-delegation/examples/parallel-research.md +337 -0
- package/opencode/skill/task-delegation/examples/sequential-build.md +504 -0
- package/opencode/skill/task-delegation/reference.md +825 -0
- package/opencode/skill/tech-stack-detection/SKILL.md +89 -0
- package/opencode/skill/tech-stack-detection/references/framework-signatures.md +598 -0
- package/opencode/skill/technical-writing/SKILL.md +190 -0
- package/opencode/skill/technical-writing/templates/adr-template.md +205 -0
- package/opencode/skill/technical-writing/templates/system-doc-template.md +380 -0
- package/opencode/skill/test-design/SKILL.md +464 -0
- package/opencode/skill/test-design/examples/test-pyramid.md +724 -0
- package/opencode/skill/testing/SKILL.md +213 -0
- package/opencode/skill/testing/examples/test-pyramid.md +724 -0
- package/opencode/skill/user-insight-synthesis/SKILL.md +576 -0
- package/opencode/skill/user-insight-synthesis/templates/research-plan-template.md +217 -0
- package/opencode/skill/user-research/SKILL.md +508 -0
- package/opencode/skill/user-research/examples/interview-questions.md +265 -0
- package/opencode/skill/user-research/examples/personas.md +267 -0
- package/opencode/skill/vibe-security/SKILL.md +654 -0
- package/package.json +45 -0
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
# DDD Pattern Implementation Examples
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
This guide provides concrete implementation examples for each DDD tactical pattern. Use these when translating domain concepts into code, reviewing existing domain models, or teaching DDD practices to a team. Examples use TypeScript pseudocode with the e-commerce domain as a consistent thread.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Entities
|
|
10
|
+
|
|
11
|
+
Entities have identity that persists over time. Two entities are equal if they share the same ID, regardless of attribute differences.
|
|
12
|
+
|
|
13
|
+
### Correct Implementation
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
class Order {
|
|
17
|
+
private readonly id: OrderId;
|
|
18
|
+
private status: OrderStatus;
|
|
19
|
+
private items: OrderItem[];
|
|
20
|
+
private customerId: CustomerId; // Reference by ID — never embed Customer object
|
|
21
|
+
|
|
22
|
+
constructor(id: OrderId, customerId: CustomerId) {
|
|
23
|
+
this.id = id;
|
|
24
|
+
this.customerId = customerId;
|
|
25
|
+
this.status = OrderStatus.Draft;
|
|
26
|
+
this.items = [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
addItem(productId: ProductId, quantity: Quantity, price: Money): void {
|
|
30
|
+
this.guardDraftStatus('add items');
|
|
31
|
+
const existing = this.items.find(i => i.productId.equals(productId));
|
|
32
|
+
if (existing) {
|
|
33
|
+
existing.increaseQuantity(quantity);
|
|
34
|
+
} else {
|
|
35
|
+
this.items.push(new OrderItem(productId, quantity, price));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
submit(): void {
|
|
40
|
+
this.guardDraftStatus('submit');
|
|
41
|
+
if (this.items.length === 0) throw new Error('Cannot submit empty order');
|
|
42
|
+
this.status = OrderStatus.Placed;
|
|
43
|
+
this.addEvent(new OrderPlaced(this.id, this.customerId, this.items, this.total));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get total(): Money {
|
|
47
|
+
return this.items.reduce(
|
|
48
|
+
(sum, item) => sum.add(item.subtotal),
|
|
49
|
+
Money.zero('USD')
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
equals(other: Order): boolean {
|
|
54
|
+
return this.id.equals(other.id); // Identity-based equality
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private guardDraftStatus(operation: string): void {
|
|
58
|
+
if (this.status !== OrderStatus.Draft) {
|
|
59
|
+
throw new Error(`Cannot ${operation} on a ${this.status} order`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Anti-Pattern: Anemic Entity
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// WRONG: State exposed, no behavior, logic lives elsewhere
|
|
69
|
+
class Order {
|
|
70
|
+
id: string;
|
|
71
|
+
status: string;
|
|
72
|
+
items: any[];
|
|
73
|
+
customerId: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// WRONG: OrderService doing what Order should do
|
|
77
|
+
class OrderService {
|
|
78
|
+
submit(order: Order): void {
|
|
79
|
+
if (order.status !== 'draft') throw new Error('...');
|
|
80
|
+
if (order.items.length === 0) throw new Error('...');
|
|
81
|
+
order.status = 'placed'; // Mutating from outside
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Why it matters**: Business rule "cannot submit an empty order" lives in `OrderService`. When a second service also needs to submit orders, the rule either gets duplicated or the service becomes a dependency. The entity cannot protect its own invariants.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Value Objects
|
|
91
|
+
|
|
92
|
+
Value objects describe characteristics. Two value objects are equal if all their attributes match. They are always immutable — operations return new instances.
|
|
93
|
+
|
|
94
|
+
### Correct Implementation: Money
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
class Money {
|
|
98
|
+
private readonly amount: number;
|
|
99
|
+
private readonly currency: Currency;
|
|
100
|
+
|
|
101
|
+
constructor(amount: number, currency: Currency) {
|
|
102
|
+
if (amount < 0) throw new Error(`Amount cannot be negative: ${amount}`);
|
|
103
|
+
if (!currency) throw new Error('Currency is required');
|
|
104
|
+
this.amount = Math.round(amount * 100) / 100; // Store in cents precision
|
|
105
|
+
this.currency = currency;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static zero(currency: string): Money {
|
|
109
|
+
return new Money(0, Currency.of(currency));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
add(other: Money): Money {
|
|
113
|
+
this.guardSameCurrency(other);
|
|
114
|
+
return new Money(this.amount + other.amount, this.currency);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
subtract(other: Money): Money {
|
|
118
|
+
this.guardSameCurrency(other);
|
|
119
|
+
const result = this.amount - other.amount;
|
|
120
|
+
return new Money(result, this.currency);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
multiply(factor: number): Money {
|
|
124
|
+
if (factor < 0) throw new Error('Cannot multiply by negative factor');
|
|
125
|
+
return new Money(this.amount * factor, this.currency);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
isGreaterThan(other: Money): boolean {
|
|
129
|
+
this.guardSameCurrency(other);
|
|
130
|
+
return this.amount > other.amount;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
format(): string {
|
|
134
|
+
return `${this.currency.symbol}${this.amount.toFixed(2)}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
equals(other: Money): boolean {
|
|
138
|
+
return this.amount === other.amount && this.currency.equals(other.currency);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private guardSameCurrency(other: Money): void {
|
|
142
|
+
if (!this.currency.equals(other.currency)) {
|
|
143
|
+
throw new Error(`Currency mismatch: ${this.currency} vs ${other.currency}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Correct Implementation: Address
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
class Address {
|
|
153
|
+
constructor(
|
|
154
|
+
public readonly street: string,
|
|
155
|
+
public readonly city: string,
|
|
156
|
+
public readonly postalCode: PostalCode,
|
|
157
|
+
public readonly country: Country
|
|
158
|
+
) {
|
|
159
|
+
if (!street.trim()) throw new Error('Street is required');
|
|
160
|
+
if (!city.trim()) throw new Error('City is required');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Returns new instance — does not mutate
|
|
164
|
+
withCity(city: string): Address {
|
|
165
|
+
return new Address(this.street, city, this.postalCode, this.country);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
equals(other: Address): boolean {
|
|
169
|
+
return (
|
|
170
|
+
this.street === other.street &&
|
|
171
|
+
this.city === other.city &&
|
|
172
|
+
this.postalCode.equals(other.postalCode) &&
|
|
173
|
+
this.country.equals(other.country)
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Anti-Pattern: Primitive Obsession
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// WRONG: Primitive types for domain concepts
|
|
183
|
+
function placeOrder(
|
|
184
|
+
customerId: string, // Is this a UUID? An email? A user number?
|
|
185
|
+
shippingStreet: string,
|
|
186
|
+
shippingCity: string,
|
|
187
|
+
shippingZip: string, // Is zip validation done? Where?
|
|
188
|
+
price: number, // USD? EUR? Cents? Dollars?
|
|
189
|
+
currency: string
|
|
190
|
+
): void { ... }
|
|
191
|
+
|
|
192
|
+
// CORRECT: Value objects carry meaning and validate themselves
|
|
193
|
+
function placeOrder(
|
|
194
|
+
customerId: CustomerId,
|
|
195
|
+
shippingAddress: Address, // Validated, complete, typed
|
|
196
|
+
price: Money // Currency-aware, non-negative
|
|
197
|
+
): void { ... }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Why it matters**: `string` for a postal code accepts `"hello"`. `PostalCode` validates format at construction. The type system communicates intent and prevents entire classes of bugs.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Aggregates
|
|
205
|
+
|
|
206
|
+
An aggregate is a cluster of domain objects treated as a single unit. The aggregate root is the only entry point — external code cannot reach inside to modify children directly.
|
|
207
|
+
|
|
208
|
+
### Correct Implementation: Order Aggregate
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
class Order { // Aggregate root
|
|
212
|
+
private readonly id: OrderId;
|
|
213
|
+
private items: OrderItem[]; // Part of aggregate — not exposed directly
|
|
214
|
+
private status: OrderStatus;
|
|
215
|
+
private readonly events: DomainEvent[] = [];
|
|
216
|
+
|
|
217
|
+
// Only the root exposes behavior — callers never touch OrderItem directly
|
|
218
|
+
addItem(productId: ProductId, quantity: Quantity, unitPrice: Money): void {
|
|
219
|
+
this.guardCanModify();
|
|
220
|
+
const item = this.findItem(productId);
|
|
221
|
+
if (item) {
|
|
222
|
+
item.increaseQuantity(quantity);
|
|
223
|
+
} else {
|
|
224
|
+
this.items.push(OrderItem.create(productId, quantity, unitPrice));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
removeItem(productId: ProductId): void {
|
|
229
|
+
this.guardCanModify();
|
|
230
|
+
const index = this.items.findIndex(i => i.productId.equals(productId));
|
|
231
|
+
if (index === -1) throw new Error(`Item ${productId} not in order`);
|
|
232
|
+
this.items.splice(index, 1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Invariant: cannot submit with zero items
|
|
236
|
+
// Invariant: cannot submit non-draft order
|
|
237
|
+
submit(): void {
|
|
238
|
+
this.guardCanModify();
|
|
239
|
+
if (this.items.length === 0) {
|
|
240
|
+
throw new DomainError('Order must have at least one item to be submitted');
|
|
241
|
+
}
|
|
242
|
+
this.status = OrderStatus.Placed;
|
|
243
|
+
this.events.push(new OrderPlaced(this.id, this.snapshot()));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Read model — callers can observe but not mutate children
|
|
247
|
+
getItems(): ReadonlyArray<OrderItemSnapshot> {
|
|
248
|
+
return this.items.map(i => i.toSnapshot());
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
pullEvents(): DomainEvent[] {
|
|
252
|
+
const events = [...this.events];
|
|
253
|
+
this.events.length = 0;
|
|
254
|
+
return events;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private findItem(productId: ProductId): OrderItem | undefined {
|
|
258
|
+
return this.items.find(i => i.productId.equals(productId));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private guardCanModify(): void {
|
|
262
|
+
if (this.status !== OrderStatus.Draft) {
|
|
263
|
+
throw new DomainError(`Cannot modify a ${this.status} order`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Aggregate: Cross-Reference by ID
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// WRONG: Holding object reference across aggregate boundary
|
|
273
|
+
class Order {
|
|
274
|
+
customer: Customer; // Entire Customer aggregate embedded — creates coupling
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// CORRECT: Reference by identity
|
|
278
|
+
class Order {
|
|
279
|
+
customerId: CustomerId; // Lookup when needed, no coupling to Customer aggregate
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// WRONG: Order directly modifying inventory
|
|
283
|
+
class OrderService {
|
|
284
|
+
async submitOrder(orderId: OrderId): Promise<void> {
|
|
285
|
+
const order = await this.orderRepo.findById(orderId);
|
|
286
|
+
const inventory = await this.inventoryRepo.findByProduct(order.productId);
|
|
287
|
+
order.submit();
|
|
288
|
+
inventory.decrement(order.quantity); // Two aggregates in one transaction
|
|
289
|
+
await this.db.transaction(() => {
|
|
290
|
+
this.orderRepo.save(order);
|
|
291
|
+
this.inventoryRepo.save(inventory); // Violates one-aggregate-per-transaction
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// CORRECT: Order raises event, inventory responds eventually
|
|
297
|
+
class OrderService {
|
|
298
|
+
async submitOrder(orderId: OrderId): Promise<void> {
|
|
299
|
+
const order = await this.orderRepo.findById(orderId);
|
|
300
|
+
order.submit(); // Produces OrderPlaced event
|
|
301
|
+
await this.orderRepo.save(order); // Single aggregate persisted
|
|
302
|
+
// OrderPlaced event → InventoryHandler reduces stock (eventual consistency)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Repositories
|
|
310
|
+
|
|
311
|
+
Repositories abstract persistence. They provide collection-like semantics — you put aggregates in, you get aggregates out — without leaking database concerns into the domain.
|
|
312
|
+
|
|
313
|
+
### Correct Implementation
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
// Interface in domain layer — no persistence technology references
|
|
317
|
+
interface OrderRepository {
|
|
318
|
+
findById(id: OrderId): Promise<Order | null>;
|
|
319
|
+
findByCustomer(customerId: CustomerId, options?: QueryOptions): Promise<Order[]>;
|
|
320
|
+
findPendingFulfillment(): Promise<Order[]>;
|
|
321
|
+
save(order: Order): Promise<void>;
|
|
322
|
+
delete(id: OrderId): Promise<void>;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Implementation in infrastructure layer
|
|
326
|
+
class PostgresOrderRepository implements OrderRepository {
|
|
327
|
+
constructor(private readonly db: DatabaseClient) {}
|
|
328
|
+
|
|
329
|
+
async findById(id: OrderId): Promise<Order | null> {
|
|
330
|
+
const row = await this.db.queryOne(
|
|
331
|
+
'SELECT * FROM orders WHERE id = $1',
|
|
332
|
+
[id.value]
|
|
333
|
+
);
|
|
334
|
+
return row ? this.reconstitute(row) : null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async save(order: Order): Promise<void> {
|
|
338
|
+
const data = this.decompose(order);
|
|
339
|
+
await this.db.upsert('orders', data);
|
|
340
|
+
// Dispatch domain events after successful save
|
|
341
|
+
const events = order.pullEvents();
|
|
342
|
+
await this.eventBus.publishAll(events);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Reconstitution rebuilds the full aggregate from raw data
|
|
346
|
+
private reconstitute(row: OrderRow): Order {
|
|
347
|
+
return Order.reconstitute({
|
|
348
|
+
id: OrderId.of(row.id),
|
|
349
|
+
customerId: CustomerId.of(row.customer_id),
|
|
350
|
+
status: OrderStatus[row.status],
|
|
351
|
+
items: row.items.map(this.reconstituteItem),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private decompose(order: Order): OrderRow {
|
|
356
|
+
// Snapshot aggregate to persistence format
|
|
357
|
+
const snapshot = order.toSnapshot();
|
|
358
|
+
return {
|
|
359
|
+
id: snapshot.id,
|
|
360
|
+
customer_id: snapshot.customerId,
|
|
361
|
+
status: snapshot.status,
|
|
362
|
+
items: snapshot.items,
|
|
363
|
+
updated_at: new Date(),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Anti-Pattern: Repository as Query Dumping Ground
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
// WRONG: Repository leaking infrastructure concerns into callers
|
|
373
|
+
interface OrderRepository {
|
|
374
|
+
findById(id: string): Promise<OrderRow>; // Returns DB row, not domain object
|
|
375
|
+
executeQuery(sql: string): Promise<any[]>; // Domain layer writing SQL
|
|
376
|
+
findWithJoin(table: string, on: string): Promise<any>; // Structural leakage
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// WRONG: Domain service building queries
|
|
380
|
+
class OrderService {
|
|
381
|
+
async getOrdersForDashboard(): Promise<Order[]> {
|
|
382
|
+
return this.db.query(`
|
|
383
|
+
SELECT o.*, c.name, p.title
|
|
384
|
+
FROM orders o
|
|
385
|
+
JOIN customers c ON o.customer_id = c.id
|
|
386
|
+
...
|
|
387
|
+
`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// CORRECT: Named queries that reveal domain intent
|
|
392
|
+
interface OrderRepository {
|
|
393
|
+
findById(id: OrderId): Promise<Order | null>;
|
|
394
|
+
findPendingFulfillment(): Promise<Order[]>; // Domain language, not SQL
|
|
395
|
+
findByCustomer(customerId: CustomerId): Promise<Order[]>;
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Domain Events
|
|
402
|
+
|
|
403
|
+
Domain events record facts about the past. They are immutable and named in past tense using domain vocabulary.
|
|
404
|
+
|
|
405
|
+
### Correct Implementation
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
// Base contract — all events share this structure
|
|
409
|
+
interface DomainEvent {
|
|
410
|
+
readonly eventId: string;
|
|
411
|
+
readonly occurredAt: Date;
|
|
412
|
+
readonly aggregateId: string;
|
|
413
|
+
readonly eventType: string;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
class OrderPlaced implements DomainEvent {
|
|
417
|
+
readonly eventId = crypto.randomUUID();
|
|
418
|
+
readonly occurredAt = new Date();
|
|
419
|
+
readonly eventType = 'OrderPlaced';
|
|
420
|
+
|
|
421
|
+
constructor(
|
|
422
|
+
readonly aggregateId: string, // orderId
|
|
423
|
+
readonly customerId: string,
|
|
424
|
+
readonly items: ReadonlyArray<OrderItemData>,
|
|
425
|
+
readonly totalAmount: MoneyData,
|
|
426
|
+
readonly placedAt: Date // Domain-meaningful timestamp
|
|
427
|
+
) {}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
class PaymentFailed implements DomainEvent {
|
|
431
|
+
readonly eventId = crypto.randomUUID();
|
|
432
|
+
readonly occurredAt = new Date();
|
|
433
|
+
readonly eventType = 'PaymentFailed';
|
|
434
|
+
|
|
435
|
+
constructor(
|
|
436
|
+
readonly aggregateId: string, // orderId
|
|
437
|
+
readonly reason: PaymentFailureReason,
|
|
438
|
+
readonly attemptedAmount: MoneyData,
|
|
439
|
+
readonly failedAt: Date
|
|
440
|
+
) {}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Aggregate collects events, repository dispatches them after save
|
|
444
|
+
class Order {
|
|
445
|
+
private events: DomainEvent[] = [];
|
|
446
|
+
|
|
447
|
+
submit(): void {
|
|
448
|
+
this.status = OrderStatus.Placed;
|
|
449
|
+
this.events.push(new OrderPlaced(
|
|
450
|
+
this.id.value,
|
|
451
|
+
this.customerId.value,
|
|
452
|
+
this.items.map(i => i.toData()),
|
|
453
|
+
this.total.toData(),
|
|
454
|
+
new Date()
|
|
455
|
+
));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
pullEvents(): DomainEvent[] {
|
|
459
|
+
const events = [...this.events];
|
|
460
|
+
this.events.length = 0;
|
|
461
|
+
return events;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Event Handlers
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
// Handler in the same service — synchronous or via in-process bus
|
|
470
|
+
class InventoryHandler {
|
|
471
|
+
async handle(event: OrderPlaced): Promise<void> {
|
|
472
|
+
for (const item of event.items) {
|
|
473
|
+
const inventory = await this.inventoryRepo.findByProduct(
|
|
474
|
+
ProductId.of(item.productId)
|
|
475
|
+
);
|
|
476
|
+
inventory.reserve(Quantity.of(item.quantity));
|
|
477
|
+
await this.inventoryRepo.save(inventory);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Handler across service boundary — integration event via message broker
|
|
483
|
+
class NotificationService {
|
|
484
|
+
async handle(event: OrderPlaced): Promise<void> {
|
|
485
|
+
const customer = await this.customerRepo.findById(
|
|
486
|
+
CustomerId.of(event.customerId)
|
|
487
|
+
);
|
|
488
|
+
await this.emailGateway.sendOrderConfirmation(customer.email, event);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Anti-Pattern: Command Masquerading as Event
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
// WRONG: Event telling other contexts what to do
|
|
497
|
+
class OrderSubmitted {
|
|
498
|
+
command = 'RESERVE_INVENTORY'; // Not an event — it's an instruction
|
|
499
|
+
productId: string;
|
|
500
|
+
quantity: number;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// WRONG: Mutable event (events are immutable facts)
|
|
504
|
+
class OrderPlaced {
|
|
505
|
+
orderId: string;
|
|
506
|
+
status: string; // Mutable — callers could change it
|
|
507
|
+
items: Item[]; // Mutable array
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// CORRECT: Immutable fact with all data needed for consumers
|
|
511
|
+
class OrderPlaced {
|
|
512
|
+
constructor(
|
|
513
|
+
readonly orderId: string,
|
|
514
|
+
readonly items: ReadonlyArray<OrderItemData>, // Immutable snapshot
|
|
515
|
+
readonly totalAmount: MoneyData,
|
|
516
|
+
readonly placedAt: Date
|
|
517
|
+
) {}
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
## Domain Services
|
|
524
|
+
|
|
525
|
+
Domain services encapsulate business logic that naturally involves multiple aggregates or domain concepts but does not belong to any single one.
|
|
526
|
+
|
|
527
|
+
### When to Use Domain Services
|
|
528
|
+
|
|
529
|
+
Use a domain service when:
|
|
530
|
+
- The operation spans multiple aggregates
|
|
531
|
+
- The operation requires external data to enforce a business rule
|
|
532
|
+
- Placing the logic in an entity would create an unnatural dependency
|
|
533
|
+
|
|
534
|
+
### Correct Implementation
|
|
535
|
+
|
|
536
|
+
```typescript
|
|
537
|
+
// QUESTION: Who calculates shipping cost?
|
|
538
|
+
// Not Order — it doesn't know shipping rates.
|
|
539
|
+
// Not ShippingRate — it doesn't know order contents.
|
|
540
|
+
// A domain service bridges them.
|
|
541
|
+
|
|
542
|
+
class ShippingCostCalculator {
|
|
543
|
+
constructor(private readonly rateRepository: ShippingRateRepository) {}
|
|
544
|
+
|
|
545
|
+
async calculate(order: Order, destination: Address): Promise<Money> {
|
|
546
|
+
const weight = order.totalWeight;
|
|
547
|
+
const zone = ShippingZone.forAddress(destination);
|
|
548
|
+
const rate = await this.rateRepository.findRate(weight, zone);
|
|
549
|
+
|
|
550
|
+
if (!rate) {
|
|
551
|
+
throw new DomainError(`No shipping rate for zone ${zone} at weight ${weight}`);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return rate.applyTo(weight);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Transfer between accounts — neither Account alone can enforce the rule
|
|
559
|
+
class FundsTransferService {
|
|
560
|
+
async transfer(
|
|
561
|
+
sourceId: AccountId,
|
|
562
|
+
destinationId: AccountId,
|
|
563
|
+
amount: Money
|
|
564
|
+
): Promise<void> {
|
|
565
|
+
const source = await this.accountRepo.findById(sourceId);
|
|
566
|
+
const destination = await this.accountRepo.findById(destinationId);
|
|
567
|
+
|
|
568
|
+
if (!source.hasSufficientFunds(amount)) {
|
|
569
|
+
throw new InsufficientFundsError(sourceId, amount);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
source.debit(amount);
|
|
573
|
+
destination.credit(amount);
|
|
574
|
+
|
|
575
|
+
// Each save publishes its own events — no cross-aggregate transaction
|
|
576
|
+
await this.accountRepo.save(source);
|
|
577
|
+
await this.accountRepo.save(destination);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Anti-Pattern: Domain Service as Catch-All
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
// WRONG: Domain service doing what the entity should do
|
|
586
|
+
class OrderService {
|
|
587
|
+
calculateTotal(order: Order): Money { // Order should own this
|
|
588
|
+
return order.items.reduce((sum, item) => sum + item.price, 0);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
isEligibleForDiscount(order: Order): boolean { // Business rule — belongs in Order
|
|
592
|
+
return order.items.length > 5;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
canBeShipped(order: Order): boolean { // Order invariant — belongs in Order
|
|
596
|
+
return order.status === 'placed';
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
**Why it matters**: When business logic lives in a service instead of the entity, every caller must import the service to perform basic operations. The domain model becomes a passive data structure — the anemic domain model anti-pattern at the service level.
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
## Bounded Contexts
|
|
606
|
+
|
|
607
|
+
Bounded contexts establish explicit boundaries where a specific domain model and ubiquitous language apply.
|
|
608
|
+
|
|
609
|
+
### Modeling the Same Concept Differently
|
|
610
|
+
|
|
611
|
+
```
|
|
612
|
+
Scenario: "Product" in an e-commerce platform
|
|
613
|
+
|
|
614
|
+
┌──────────────────────────┐ ┌──────────────────────────┐ ┌──────────────────────────┐
|
|
615
|
+
│ Catalog Context │ │ Inventory Context │ │ Pricing Context │
|
|
616
|
+
├──────────────────────────┤ ├──────────────────────────┤ ├──────────────────────────┤
|
|
617
|
+
│ Product: │ │ StockKeepingUnit: │ │ PricedItem: │
|
|
618
|
+
│ - name │ │ - sku │ │ - productId │
|
|
619
|
+
│ - description │ │ - warehouseLocation │ │ - basePrice │
|
|
620
|
+
│ - images │ │ - quantity │ │ - discountRules │
|
|
621
|
+
│ - categories │ │ - reorderThreshold │ │ - taxCategory │
|
|
622
|
+
│ - specifications │ │ - supplierId │ │ - effectiveFrom │
|
|
623
|
+
└──────────────────────────┘ └──────────────────────────┘ └──────────────────────────┘
|
|
624
|
+
│ │ │
|
|
625
|
+
└──────────────────────────────┴──────────────────────────────┘
|
|
626
|
+
Shared identifier: productId
|
|
627
|
+
Different model, different language, different team
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
### Anti-Corruption Layer
|
|
631
|
+
|
|
632
|
+
```typescript
|
|
633
|
+
// External supplier API speaks a different language
|
|
634
|
+
// ACL translates without polluting the domain model
|
|
635
|
+
|
|
636
|
+
interface SupplierApi {
|
|
637
|
+
getProductData(ean: string): Promise<SupplierProductRecord>;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Raw supplier vocabulary — not your domain's language
|
|
641
|
+
interface SupplierProductRecord {
|
|
642
|
+
GTIN: string;
|
|
643
|
+
PROD_NAME: string;
|
|
644
|
+
WAREHOUSE_CODE: string;
|
|
645
|
+
AVAIL_QTY: number;
|
|
646
|
+
REORDER_LVL: number;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ACL translates to your ubiquitous language
|
|
650
|
+
class SupplierInventoryAdapter {
|
|
651
|
+
constructor(private readonly supplierApi: SupplierApi) {}
|
|
652
|
+
|
|
653
|
+
async getStockLevel(productId: ProductId): Promise<StockLevel> {
|
|
654
|
+
const raw = await this.supplierApi.getProductData(productId.ean);
|
|
655
|
+
return new StockLevel(
|
|
656
|
+
ProductId.of(raw.GTIN),
|
|
657
|
+
Quantity.of(raw.AVAIL_QTY),
|
|
658
|
+
Quantity.of(raw.REORDER_LVL),
|
|
659
|
+
WarehouseCode.of(raw.WAREHOUSE_CODE)
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Domain service uses your language — supplier details hidden behind adapter
|
|
665
|
+
class InventoryService {
|
|
666
|
+
async checkReorderNeeded(productId: ProductId): Promise<boolean> {
|
|
667
|
+
const stock = await this.supplierAdapter.getStockLevel(productId);
|
|
668
|
+
return stock.isBelowReorderThreshold();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## Common Mistakes Summary
|
|
676
|
+
|
|
677
|
+
| Mistake | Symptom | Fix |
|
|
678
|
+
|---------|---------|-----|
|
|
679
|
+
| **Anemic domain model** | Services contain all business logic, entities are data bags | Move business rules into entity methods |
|
|
680
|
+
| **Oversized aggregates** | Lock contention, slow loads, unrelated data changes together | Split on invariant boundaries, use eventual consistency |
|
|
681
|
+
| **Primitive obsession** | `string` for email, `number` for price, no validation | Create value objects for domain concepts |
|
|
682
|
+
| **Cross-aggregate references** | Entity holds reference to another aggregate's object | Reference by ID only |
|
|
683
|
+
| **Multiple aggregates per transaction** | Transaction spans two repositories | Use domain events and eventual consistency |
|
|
684
|
+
| **Repository as query service** | Repository has 30 methods, returns DTOs or raw rows | Separate read models from aggregate repositories |
|
|
685
|
+
| **Commands masquerading as events** | Event names like `SendConfirmationEmail` | Name events in past tense: `OrderPlaced` |
|
|
686
|
+
| **Business logic in event handlers** | Handlers make business decisions, not just reactions | Handlers update state only; business rules stay in aggregates |
|
|
687
|
+
| **Missing ubiquitous language** | Code uses technical terms, domain experts can't read it | Rename classes and methods to match domain vocabulary |
|
|
688
|
+
| **Shared database between contexts** | Two services join tables across context boundaries | Each context owns its data; integrate via events or APIs |
|