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,465 @@
|
|
|
1
|
+
# Aggregate Design Guide
|
|
2
|
+
|
|
3
|
+
Advanced heuristics for defining aggregate boundaries, sizing aggregates correctly, and choosing consistency strategies. Load this when the SKILL.md's high-level rules are insufficient for the decision at hand.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Aggregate Boundaries
|
|
8
|
+
|
|
9
|
+
Aggregate boundaries exist to protect invariants — the business rules that must always be true. The primary question when drawing a boundary is: **which objects must change together to keep a business rule intact?**
|
|
10
|
+
|
|
11
|
+
### The Invariant Test
|
|
12
|
+
|
|
13
|
+
For each proposed aggregate boundary, identify the invariants it protects:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Example: Should OrderItem be inside Order or its own aggregate?
|
|
17
|
+
|
|
18
|
+
Invariant: "An order's total cannot exceed the customer's credit limit"
|
|
19
|
+
|
|
20
|
+
Test:
|
|
21
|
+
- Can we check this rule with only the Order? YES — Order knows its items and total.
|
|
22
|
+
- Does changing an OrderItem require validating Order-level rules? YES.
|
|
23
|
+
- Can OrderItem exist independently with its own lifecycle? NO — it only exists for an order.
|
|
24
|
+
|
|
25
|
+
Decision: OrderItem is INSIDE the Order aggregate.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
Example: Should Order be inside Customer or its own aggregate?
|
|
30
|
+
|
|
31
|
+
Invariant: "A customer cannot have more than 10 active orders simultaneously"
|
|
32
|
+
|
|
33
|
+
Test:
|
|
34
|
+
- Must Order and Customer change together to protect this rule? NO.
|
|
35
|
+
- Can we check this rule by querying the count of Orders for a CustomerId? YES.
|
|
36
|
+
- Does Customer have a distinct lifecycle from Order? YES.
|
|
37
|
+
|
|
38
|
+
Decision: Order is a SEPARATE aggregate, referenced from Customer by ID.
|
|
39
|
+
The invariant is enforced via a domain service or application-level check before creating an Order.
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Aggregate Design Canvas
|
|
43
|
+
|
|
44
|
+
When designing a new aggregate, work through this canvas:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
48
|
+
│ AGGREGATE NAME: │
|
|
49
|
+
├─────────────────────────────────────────────────────────────┤
|
|
50
|
+
│ INVARIANTS (rules that must always hold): │
|
|
51
|
+
│ 1. │
|
|
52
|
+
│ 2. │
|
|
53
|
+
├─────────────────────────────────────────────────────────────┤
|
|
54
|
+
│ AGGREGATE ROOT (single entry point for all changes): │
|
|
55
|
+
│ │
|
|
56
|
+
├─────────────────────────────────────────────────────────────┤
|
|
57
|
+
│ INSIDE THE BOUNDARY (change together to protect invariants):│
|
|
58
|
+
│ Entities: │
|
|
59
|
+
│ Value Objects: │
|
|
60
|
+
├─────────────────────────────────────────────────────────────┤
|
|
61
|
+
│ OUTSIDE THE BOUNDARY (referenced by ID): │
|
|
62
|
+
│ │
|
|
63
|
+
├─────────────────────────────────────────────────────────────┤
|
|
64
|
+
│ DOMAIN EVENTS (what this aggregate announces): │
|
|
65
|
+
│ │
|
|
66
|
+
├─────────────────────────────────────────────────────────────┤
|
|
67
|
+
│ CONSISTENCY TYPE: │
|
|
68
|
+
│ [ ] Transactional — invariants protected within boundary │
|
|
69
|
+
│ [ ] Eventual — consistency with other aggregates via events│
|
|
70
|
+
└─────────────────────────────────────────────────────────────┘
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Common Boundary Mistakes
|
|
74
|
+
|
|
75
|
+
**Grouping by noun instead of invariant**
|
|
76
|
+
|
|
77
|
+
The mistake is asking "what belongs to an order?" rather than "which objects must change together to enforce order rules?"
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
// WRONG: Grouped by association
|
|
81
|
+
class Order {
|
|
82
|
+
customer: Customer; // Does Customer's email change when Order changes? No.
|
|
83
|
+
payments: Payment[]; // Does Payment share Order's invariants? No.
|
|
84
|
+
shipments: Shipment[]; // Can Shipment change independently? Yes.
|
|
85
|
+
reviews: Review[]; // No shared invariants with Order.
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// CORRECT: Grouped by invariant
|
|
89
|
+
class Order { // Invariant: item total <= approved budget
|
|
90
|
+
items: OrderItem[]; // Must change with Order to enforce the rule
|
|
91
|
+
discountCode: DiscountCode; // Applied at order level, affects total
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Separate aggregates, referenced by OrderId
|
|
95
|
+
class Payment { orderId: OrderId; ... }
|
|
96
|
+
class Shipment { orderId: OrderId; ... }
|
|
97
|
+
class Review { orderId: OrderId; ... }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Including historical data**
|
|
101
|
+
|
|
102
|
+
Historical records rarely share invariants with the current state. They grow unbounded and should be separate.
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
// WRONG: History inside aggregate
|
|
106
|
+
class Account {
|
|
107
|
+
balance: Money;
|
|
108
|
+
transactions: Transaction[]; // Could be millions — never inspected for invariants
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// CORRECT: Ledger as separate aggregate
|
|
112
|
+
class Account {
|
|
113
|
+
balance: Money; // Current state
|
|
114
|
+
// Invariant: balance = sum of credits - sum of debits
|
|
115
|
+
// Enforced by debit() and credit() methods, not by inspecting transaction history
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class Transaction {
|
|
119
|
+
accountId: AccountId; // Reference by ID
|
|
120
|
+
amount: Money;
|
|
121
|
+
type: TransactionType;
|
|
122
|
+
occurredAt: Date;
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Aggregate Sizing
|
|
129
|
+
|
|
130
|
+
Start with the smallest possible aggregate — usually a single entity — and expand only when an invariant cannot be protected otherwise.
|
|
131
|
+
|
|
132
|
+
### Size Signals
|
|
133
|
+
|
|
134
|
+
**Signals that an aggregate is too large:**
|
|
135
|
+
|
|
136
|
+
| Signal | What It Means |
|
|
137
|
+
|--------|--------------|
|
|
138
|
+
| Optimistic lock conflicts on concurrent edits | Multiple users editing unrelated parts simultaneously |
|
|
139
|
+
| Loading thousands of rows for a simple operation | Aggregate includes unbounded collections |
|
|
140
|
+
| Transactional failures involving unrelated data | Scope is wider than invariants require |
|
|
141
|
+
| Slow reconstitution from the repository | Too many child objects |
|
|
142
|
+
| Cross-cutting edits by different bounded contexts | Boundary is in the wrong place |
|
|
143
|
+
|
|
144
|
+
**Signals that an aggregate is too small:**
|
|
145
|
+
|
|
146
|
+
| Signal | What It Means |
|
|
147
|
+
|--------|--------------|
|
|
148
|
+
| Business rules scattered across domain services | Invariant spans objects not in the same boundary |
|
|
149
|
+
| Application code enforcing consistency across multiple saves | Transaction script replacing domain logic |
|
|
150
|
+
| "Eventual" consistency used where the business demands immediate | Boundary was split too aggressively |
|
|
151
|
+
|
|
152
|
+
### Sizing Heuristics
|
|
153
|
+
|
|
154
|
+
**1. Default to single-entity aggregates**
|
|
155
|
+
|
|
156
|
+
Most aggregates start as a single entity. Add children only when a specific invariant requires it.
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
// Start here
|
|
160
|
+
class Order {
|
|
161
|
+
id: OrderId;
|
|
162
|
+
status: OrderStatus;
|
|
163
|
+
customerId: CustomerId; // Reference — not inside
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Expand when: "total must be recalculated across all items atomically"
|
|
167
|
+
class Order {
|
|
168
|
+
id: OrderId;
|
|
169
|
+
status: OrderStatus;
|
|
170
|
+
customerId: CustomerId;
|
|
171
|
+
items: OrderItem[]; // Now inside — total invariant requires it
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**2. Limit unbounded collections**
|
|
176
|
+
|
|
177
|
+
If a collection can grow without bound, it must not be inside the aggregate.
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
// BAD: Blog with all comments inside
|
|
181
|
+
class BlogPost {
|
|
182
|
+
title: string;
|
|
183
|
+
body: string;
|
|
184
|
+
comments: Comment[]; // Could be 10,000 — all loaded for every post operation
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// GOOD: Comment is its own aggregate
|
|
188
|
+
class BlogPost {
|
|
189
|
+
title: string;
|
|
190
|
+
body: string;
|
|
191
|
+
commentCount: number; // Denormalized count for display — updated via event
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
class Comment {
|
|
195
|
+
postId: PostId; // Reference — not embedded in post
|
|
196
|
+
body: string;
|
|
197
|
+
approvedAt: Date | null;
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**3. Prefer eventual consistency at aggregate boundaries**
|
|
202
|
+
|
|
203
|
+
Business rarely demands immediate consistency across aggregates. Clarify with domain experts whether "immediately" means within the same transaction or "shortly thereafter."
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
Question to ask: "If placing an order and reserving inventory happened within
|
|
207
|
+
2 seconds of each other — would that be acceptable to the business?"
|
|
208
|
+
|
|
209
|
+
If YES → eventual consistency, separate aggregates, domain events
|
|
210
|
+
If NO → investigate why. Usually it's a UI concern, not a true invariant.
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Consistency Rules
|
|
216
|
+
|
|
217
|
+
### Transactional Consistency
|
|
218
|
+
|
|
219
|
+
Use when: **invariants must be true the instant a command completes.**
|
|
220
|
+
|
|
221
|
+
Scope: within a single aggregate, in a single transaction.
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
Application layer — one aggregate per command:
|
|
225
|
+
|
|
226
|
+
async function addItemToOrder(command: AddItemCommand): Promise<void> {
|
|
227
|
+
const order = await this.orderRepo.findById(command.orderId);
|
|
228
|
+
order.addItem(command.productId, command.quantity, command.price);
|
|
229
|
+
await this.orderRepo.save(order); // Atomic: load → mutate → save
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Eventual Consistency
|
|
234
|
+
|
|
235
|
+
Use when: **consistency between aggregates can be achieved asynchronously.**
|
|
236
|
+
|
|
237
|
+
Scope: across aggregates, within or across services.
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
Pattern: publish → subscribe → update own aggregate
|
|
241
|
+
|
|
242
|
+
// Step 1: Order aggregate raises an event
|
|
243
|
+
class Order {
|
|
244
|
+
submit(): void {
|
|
245
|
+
this.status = OrderStatus.Placed;
|
|
246
|
+
this.events.push(new OrderPlaced(this.id, this.items, this.total));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Step 2: Repository publishes after successful save
|
|
251
|
+
class PostgresOrderRepository {
|
|
252
|
+
async save(order: Order): Promise<void> {
|
|
253
|
+
await this.db.save(this.decompose(order));
|
|
254
|
+
await this.eventBus.publishAll(order.pullEvents());
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Step 3: Inventory aggregate handles event in its own transaction
|
|
259
|
+
class InventoryProjection {
|
|
260
|
+
async on(event: OrderPlaced): Promise<void> {
|
|
261
|
+
for (const item of event.items) {
|
|
262
|
+
const stock = await this.stockRepo.findByProduct(item.productId);
|
|
263
|
+
stock.reserve(item.quantity);
|
|
264
|
+
await this.stockRepo.save(stock); // Own aggregate, own transaction
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Choosing Between Transactional and Eventual
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
Decision tree:
|
|
274
|
+
|
|
275
|
+
1. Are the objects part of the same aggregate?
|
|
276
|
+
YES → Transactional (ACID within aggregate boundary)
|
|
277
|
+
NO → Continue ↓
|
|
278
|
+
|
|
279
|
+
2. Does the business genuinely require atomicity across these objects?
|
|
280
|
+
YES → Reconsider the aggregate boundary — they may belong together
|
|
281
|
+
NO → Continue ↓
|
|
282
|
+
|
|
283
|
+
3. Is latency measured in seconds acceptable?
|
|
284
|
+
YES → Eventual consistency via domain events
|
|
285
|
+
NO → Investigate. Usually a UI expectation, not a true business requirement.
|
|
286
|
+
|
|
287
|
+
4. Does failure in the secondary update require rollback of the primary?
|
|
288
|
+
YES → Saga with compensation (not distributed transaction)
|
|
289
|
+
NO → Eventual consistency, idempotent handlers, retry on failure
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Eventual Consistency Between Aggregates
|
|
295
|
+
|
|
296
|
+
### Idempotent Event Handlers
|
|
297
|
+
|
|
298
|
+
Handlers must be safe to call more than once. Message brokers deliver at-least-once; handlers must not double-apply effects.
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
class InventoryHandler {
|
|
302
|
+
async on(event: OrderPlaced): Promise<void> {
|
|
303
|
+
// Guard: skip if already processed this event
|
|
304
|
+
const processed = await this.processedEvents.contains(event.eventId);
|
|
305
|
+
if (processed) return;
|
|
306
|
+
|
|
307
|
+
for (const item of event.items) {
|
|
308
|
+
const stock = await this.stockRepo.findByProduct(item.productId);
|
|
309
|
+
stock.reserve(item.quantity);
|
|
310
|
+
await this.stockRepo.save(stock);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
await this.processedEvents.record(event.eventId);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Handling Failures
|
|
319
|
+
|
|
320
|
+
When a handler fails, the business needs a clear answer: is the failure retryable or fatal?
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
Retryable failures (retry with backoff):
|
|
324
|
+
- Network timeout
|
|
325
|
+
- Downstream service unavailable
|
|
326
|
+
- Optimistic lock conflict
|
|
327
|
+
|
|
328
|
+
Fatal failures (dead letter queue + alert):
|
|
329
|
+
- Invalid event schema — event is malformed
|
|
330
|
+
- Business rule violation — inventory does not exist for the product
|
|
331
|
+
- Permanent downstream rejection — payment provider blacklisted the account
|
|
332
|
+
|
|
333
|
+
Pattern:
|
|
334
|
+
try {
|
|
335
|
+
await handler.on(event)
|
|
336
|
+
} catch (RetryableError) {
|
|
337
|
+
await queue.nack(event) // Return to queue for retry
|
|
338
|
+
} catch (FatalError) {
|
|
339
|
+
await deadLetterQueue.send(event) // Human review required
|
|
340
|
+
await alerting.notify(error)
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Eventual Consistency and User Experience
|
|
345
|
+
|
|
346
|
+
When the UI must reflect a consistent state before eventual handlers complete, use optimistic updates:
|
|
347
|
+
|
|
348
|
+
```
|
|
349
|
+
Pattern: Update read model immediately, correct if event fails
|
|
350
|
+
|
|
351
|
+
1. User places order → UI shows "Order Placed" immediately
|
|
352
|
+
2. OrderPlaced event → InventoryHandler runs asynchronously
|
|
353
|
+
3. If inventory is insufficient → OrderFailed event raised
|
|
354
|
+
4. UI receives OrderFailed notification → shows error, reverts display
|
|
355
|
+
|
|
356
|
+
This preserves responsiveness without requiring synchronous cross-aggregate updates.
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Saga Pattern for Multi-Step Processes
|
|
362
|
+
|
|
363
|
+
Sagas coordinate a sequence of aggregate updates with explicit compensation when steps fail.
|
|
364
|
+
|
|
365
|
+
### Choreography-Based Saga
|
|
366
|
+
|
|
367
|
+
Each aggregate reacts to events and publishes its own. No central coordinator.
|
|
368
|
+
|
|
369
|
+
```
|
|
370
|
+
OrderPlaced
|
|
371
|
+
→ InventoryHandler: reserves stock → InventoryReserved
|
|
372
|
+
→ InventoryReserved → PaymentHandler: charges card → PaymentCharged
|
|
373
|
+
→ PaymentCharged → FulfillmentHandler: ships order → OrderShipped
|
|
374
|
+
|
|
375
|
+
Compensation chain on PaymentFailed:
|
|
376
|
+
PaymentFailed → InventoryHandler: releases reservation → InventoryReleased
|
|
377
|
+
InventoryReleased → OrderHandler: cancels order → OrderCancelled
|
|
378
|
+
|
|
379
|
+
Tradeoff: Simple to implement, difficult to trace the full process.
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Orchestration-Based Saga
|
|
383
|
+
|
|
384
|
+
A saga object drives the steps and handles compensation explicitly.
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
class OrderFulfillmentSaga {
|
|
388
|
+
private step: FulfillmentStep = FulfillmentStep.ReserveInventory;
|
|
389
|
+
|
|
390
|
+
async handle(event: DomainEvent): Promise<void> {
|
|
391
|
+
switch (this.step) {
|
|
392
|
+
case FulfillmentStep.ReserveInventory:
|
|
393
|
+
if (event instanceof OrderPlaced) {
|
|
394
|
+
await this.inventoryService.reserve(event.items);
|
|
395
|
+
this.step = FulfillmentStep.ChargePayment;
|
|
396
|
+
}
|
|
397
|
+
break;
|
|
398
|
+
|
|
399
|
+
case FulfillmentStep.ChargePayment:
|
|
400
|
+
if (event instanceof InventoryReserved) {
|
|
401
|
+
await this.paymentService.charge(event.orderId, event.amount);
|
|
402
|
+
this.step = FulfillmentStep.Ship;
|
|
403
|
+
}
|
|
404
|
+
if (event instanceof InventoryReservationFailed) {
|
|
405
|
+
await this.orderService.cancel(event.orderId, CancelReason.OutOfStock);
|
|
406
|
+
this.step = FulfillmentStep.Compensating;
|
|
407
|
+
}
|
|
408
|
+
break;
|
|
409
|
+
|
|
410
|
+
case FulfillmentStep.Ship:
|
|
411
|
+
if (event instanceof PaymentCharged) {
|
|
412
|
+
await this.fulfillmentService.ship(event.orderId);
|
|
413
|
+
this.step = FulfillmentStep.Complete;
|
|
414
|
+
}
|
|
415
|
+
if (event instanceof PaymentFailed) {
|
|
416
|
+
await this.inventoryService.release(event.orderId);
|
|
417
|
+
await this.orderService.cancel(event.orderId, CancelReason.PaymentFailed);
|
|
418
|
+
this.step = FulfillmentStep.Compensating;
|
|
419
|
+
}
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### When to Use Each
|
|
427
|
+
|
|
428
|
+
| Approach | Use When |
|
|
429
|
+
|----------|----------|
|
|
430
|
+
| **Choreography** | Simple 2-3 step processes, teams can coordinate on event contracts |
|
|
431
|
+
| **Orchestration** | Complex multi-step processes, compensation logic is non-trivial |
|
|
432
|
+
| **Neither** | Steps are fast, can be synchronous, business requires atomicity |
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## Rules of Thumb
|
|
437
|
+
|
|
438
|
+
These are practical defaults, not laws. Use them as starting points and adjust based on invariants.
|
|
439
|
+
|
|
440
|
+
```
|
|
441
|
+
1. Default to one entity per aggregate.
|
|
442
|
+
Expand only when you can name the specific invariant that requires it.
|
|
443
|
+
|
|
444
|
+
2. If you need to update two aggregates simultaneously, question the boundary.
|
|
445
|
+
Either they should be one aggregate, or eventual consistency is acceptable.
|
|
446
|
+
|
|
447
|
+
3. An aggregate that is never loaded alone is a smell.
|
|
448
|
+
If you always load Order with Customer together, Customer may belong inside Order.
|
|
449
|
+
Or the operation should live in a different bounded context.
|
|
450
|
+
|
|
451
|
+
4. Prefer many small aggregates over one large one.
|
|
452
|
+
A large aggregate is a coordination bottleneck under concurrent load.
|
|
453
|
+
|
|
454
|
+
5. A repository method that returns a list of thousands is wrong.
|
|
455
|
+
Either the collection shouldn't be in the aggregate, or you need a read model.
|
|
456
|
+
|
|
457
|
+
6. Use value objects for anything described by its attributes, not its identity.
|
|
458
|
+
If two instances with the same data are interchangeable, it's a value object.
|
|
459
|
+
|
|
460
|
+
7. Events describe what happened, not what should happen next.
|
|
461
|
+
"OrderPlaced" is correct. "SendConfirmationEmail" is a command, not an event.
|
|
462
|
+
|
|
463
|
+
8. Aggregate boundaries are not permanent.
|
|
464
|
+
Model based on current understanding. Refactor when invariants become clearer.
|
|
465
|
+
```
|