@martinffx/atelier 1.2.1 → 1.3.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/CHANGELOG.md +27 -0
- package/README.md +22 -24
- package/agents/architect.md +1 -1
- package/agents/oracle.md +1 -1
- package/dist/atelier.js +12 -14
- package/package.json +1 -1
- package/skills/code-commit/SKILL.md +5 -13
- package/skills/code-pull-request/SKILL.md +36 -0
- package/skills/code-pull-request/references/common.md +76 -0
- package/skills/code-pull-request/references/create-pr.md +122 -0
- package/skills/code-pull-request/references/default-template.md +25 -0
- package/skills/code-pull-request/references/leave-comment.md +53 -0
- package/skills/code-pull-request/references/merge-pr.md +94 -0
- package/skills/code-pull-request/references/read-comments.md +61 -0
- package/skills/code-pull-request/references/respond-to-comment.md +66 -0
- package/skills/code-review/SKILL.md +21 -12
- package/skills/code-review/references/reviewers.md +142 -41
- package/skills/code-review/references/rq.md +48 -32
- package/skills/oracle-debug/SKILL.md +218 -0
- package/skills/{code-debug → oracle-debug}/references/bug-patterns.md +3 -3
- package/skills/oracle-domain-modelling/SKILL.md +84 -0
- package/skills/oracle-grill-me/SKILL.md +20 -0
- package/skills/spec-brainstorm/SKILL.md +91 -71
- package/skills/spec-finish/SKILL.md +20 -14
- package/skills/spec-implement/SKILL.md +3 -2
- package/skills/spec-orchestrator/SKILL.md +4 -15
- package/skills/spec-plan/SKILL.md +1 -1
- package/agents/scout.md +0 -40
- package/skills/code-commit/references/pr-workflow.md +0 -145
- package/skills/code-debug/SKILL.md +0 -199
- package/skills/code-docs/SKILL.md +0 -171
- package/skills/code-docs/references/api-docs.md +0 -116
- package/skills/code-docs/references/changelog.md +0 -64
- package/skills/code-docs/references/readme-structure.md +0 -94
- package/skills/oracle-architect/SKILL.md +0 -327
- package/skills/oracle-architect/references/api-design.md +0 -345
- package/skills/oracle-architect/references/data-modeling.md +0 -318
- package/skills/oracle-architect/references/ddd-patterns.md +0 -348
- package/skills/oracle-architect/references/interface-design.md +0 -203
- package/skills/oracle-challenge/SKILL.md +0 -126
- package/skills/oracle-doubt/SKILL.md +0 -252
- package/skills/oracle-doubt/references/adversarial-prompt.md +0 -47
- package/skills/oracle-grillme/SKILL.md +0 -160
- package/skills/oracle-security/SKILL.md +0 -225
- package/skills/oracle-security/references/security-patterns.md +0 -236
- package/skills/oracle-security/references/threat-modeling-guide.md +0 -137
- package/skills/oracle-testing/SKILL.md +0 -237
- package/skills/oracle-testing/references/anti-patterns.md +0 -618
- package/skills/oracle-testing/references/boundaries.md +0 -579
- package/skills/oracle-testing/references/core-testing.md +0 -691
- package/skills/oracle-testing/references/edge-testing.md +0 -794
- package/skills/oracle-testing/references/stub-driven-tdd.md +0 -322
- package/skills/oracle-thinkdeep/SKILL.md +0 -164
- /package/skills/{code-debug → oracle-debug}/references/language-tools.md +0 -0
- /package/skills/{code-debug → oracle-debug}/references/techniques.md +0 -0
- /package/skills/{oracle-grillme → oracle-domain-modelling}/references/ADR-FORMAT.md +0 -0
- /package/skills/{oracle-grillme → oracle-domain-modelling}/references/CONTEXT-FORMAT.md +0 -0
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
# Data Modeling Reference
|
|
2
|
-
|
|
3
|
-
Entity design principles, schema patterns, and access pattern optimization.
|
|
4
|
-
|
|
5
|
-
## Entity Design
|
|
6
|
-
|
|
7
|
-
### Design Process
|
|
8
|
-
1. Start with domain concepts from product requirements
|
|
9
|
-
2. Identify aggregates and their boundaries
|
|
10
|
-
3. Define attributes and relationships
|
|
11
|
-
4. Add validation rules and invariants
|
|
12
|
-
5. Include transformation methods (fromRequest, toRecord, toResponse)
|
|
13
|
-
|
|
14
|
-
### Entity Structure
|
|
15
|
-
|
|
16
|
-
**Core transformation methods:**
|
|
17
|
-
```typescript
|
|
18
|
-
class Order {
|
|
19
|
-
// Create from external request
|
|
20
|
-
static fromRequest(req: CreateOrderRequest): Order
|
|
21
|
-
|
|
22
|
-
// Create from database record
|
|
23
|
-
static fromRecord(record: OrderRecord): Order
|
|
24
|
-
|
|
25
|
-
// Convert to database record
|
|
26
|
-
toRecord(): OrderRecord
|
|
27
|
-
|
|
28
|
-
// Convert to API response
|
|
29
|
-
toResponse(): OrderResponse
|
|
30
|
-
|
|
31
|
-
// Validate invariants
|
|
32
|
-
validate(): Result<Order>
|
|
33
|
-
}
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### Example Entity Design
|
|
37
|
-
|
|
38
|
-
```typescript
|
|
39
|
-
class Order {
|
|
40
|
-
constructor(
|
|
41
|
-
public readonly id: string,
|
|
42
|
-
public readonly customerId: string,
|
|
43
|
-
public readonly items: OrderItem[],
|
|
44
|
-
public readonly status: OrderStatus,
|
|
45
|
-
public readonly total: number,
|
|
46
|
-
public readonly createdAt: Date,
|
|
47
|
-
public readonly metadata: OrderMetadata
|
|
48
|
-
) {}
|
|
49
|
-
|
|
50
|
-
static fromRequest(req: CreateOrderRequest): Order {
|
|
51
|
-
return new Order(
|
|
52
|
-
generateId(),
|
|
53
|
-
req.customerId,
|
|
54
|
-
req.items.map(i => new OrderItem(i)),
|
|
55
|
-
'pending',
|
|
56
|
-
req.items.reduce((sum, i) => sum + i.price * i.quantity, 0),
|
|
57
|
-
new Date(),
|
|
58
|
-
{ source: req.source || 'web' }
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
toRecord(): OrderRecord {
|
|
63
|
-
return {
|
|
64
|
-
id: this.id,
|
|
65
|
-
customer_id: this.customerId,
|
|
66
|
-
items: JSON.stringify(this.items),
|
|
67
|
-
status: this.status,
|
|
68
|
-
total: this.total,
|
|
69
|
-
created_at: this.createdAt.toISOString(),
|
|
70
|
-
metadata: this.metadata
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
toResponse(): OrderResponse {
|
|
75
|
-
return {
|
|
76
|
-
id: this.id,
|
|
77
|
-
customerId: this.customerId,
|
|
78
|
-
items: this.items,
|
|
79
|
-
status: this.status,
|
|
80
|
-
total: this.total,
|
|
81
|
-
createdAt: this.createdAt.toISOString()
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
validate(): Result<Order> {
|
|
86
|
-
if (this.items.length === 0) {
|
|
87
|
-
return Err('Order must have at least one item');
|
|
88
|
-
}
|
|
89
|
-
if (this.total < 0) {
|
|
90
|
-
return Err('Order total cannot be negative');
|
|
91
|
-
}
|
|
92
|
-
return Ok(this);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## Schema Patterns
|
|
98
|
-
|
|
99
|
-
### Relational Design
|
|
100
|
-
|
|
101
|
-
**Orders table:**
|
|
102
|
-
```sql
|
|
103
|
-
CREATE TABLE orders (
|
|
104
|
-
id UUID PRIMARY KEY,
|
|
105
|
-
customer_id UUID NOT NULL REFERENCES customers(id),
|
|
106
|
-
status VARCHAR(20) NOT NULL CHECK (status IN ('pending', 'confirmed', 'shipped', 'delivered')),
|
|
107
|
-
total DECIMAL(10,2) NOT NULL,
|
|
108
|
-
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
109
|
-
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
CREATE INDEX idx_orders_customer ON orders(customer_id);
|
|
113
|
-
CREATE INDEX idx_orders_status ON orders(status);
|
|
114
|
-
CREATE INDEX idx_orders_created ON orders(created_at DESC);
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
**Order items (normalized):**
|
|
118
|
-
```sql
|
|
119
|
-
CREATE TABLE order_items (
|
|
120
|
-
id UUID PRIMARY KEY,
|
|
121
|
-
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
|
|
122
|
-
product_id UUID NOT NULL,
|
|
123
|
-
quantity INT NOT NULL CHECK (quantity > 0),
|
|
124
|
-
price DECIMAL(10,2) NOT NULL,
|
|
125
|
-
UNIQUE(order_id, product_id)
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
CREATE INDEX idx_order_items_order ON order_items(order_id);
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### DynamoDB Single Table Design
|
|
132
|
-
|
|
133
|
-
**Items structure:**
|
|
134
|
-
```typescript
|
|
135
|
-
{
|
|
136
|
-
PK: 'ORDER#123',
|
|
137
|
-
SK: 'ORDER#123',
|
|
138
|
-
type: 'Order',
|
|
139
|
-
customerId: 'CUST#456',
|
|
140
|
-
status: 'pending',
|
|
141
|
-
total: 99.99,
|
|
142
|
-
items: [...],
|
|
143
|
-
createdAt: '2024-01-21T10:00:00Z',
|
|
144
|
-
|
|
145
|
-
// GSI for customer lookup
|
|
146
|
-
GSI1PK: 'CUST#456',
|
|
147
|
-
GSI1SK: 'ORDER#2024-01-21T10:00:00Z',
|
|
148
|
-
|
|
149
|
-
// GSI for status lookup
|
|
150
|
-
GSI2PK: 'pending',
|
|
151
|
-
GSI2SK: 'ORDER#2024-01-21T10:00:00Z'
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
{
|
|
155
|
-
PK: 'ORDER#123',
|
|
156
|
-
SK: 'ITEM#item-001',
|
|
157
|
-
type: 'OrderItem',
|
|
158
|
-
productId: 'PROD#999',
|
|
159
|
-
quantity: 2,
|
|
160
|
-
price: 49.99
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
**Key design:**
|
|
165
|
-
- PK = partition key (primary lookup)
|
|
166
|
-
- SK = sort key (range queries)
|
|
167
|
-
- GSI = global secondary index (alternative queries)
|
|
168
|
-
|
|
169
|
-
## Access Patterns
|
|
170
|
-
|
|
171
|
-
### Define Patterns First
|
|
172
|
-
|
|
173
|
-
List all data access patterns **before** designing schema:
|
|
174
|
-
|
|
175
|
-
1. **Find order by ID**
|
|
176
|
-
- Query: `PK = ORDER#123, SK = ORDER#123`
|
|
177
|
-
- Index: Primary key
|
|
178
|
-
|
|
179
|
-
2. **List orders by customer**
|
|
180
|
-
- Query: `GSI1PK = CUST#456`
|
|
181
|
-
- Index: Global secondary index
|
|
182
|
-
|
|
183
|
-
3. **List pending orders by date**
|
|
184
|
-
- Query: `GSI2PK = pending, GSI2SK > date`
|
|
185
|
-
- Index: Global secondary index sorted by date
|
|
186
|
-
|
|
187
|
-
4. **Count orders by status**
|
|
188
|
-
- Query: Scan with filter (or GSI)
|
|
189
|
-
- Performance: Consider status table
|
|
190
|
-
|
|
191
|
-
5. **Get recent orders for customer**
|
|
192
|
-
- Query: `GSI1PK = CUST#456` sorted by date DESC
|
|
193
|
-
- Index: Global secondary index
|
|
194
|
-
|
|
195
|
-
### Query Optimization
|
|
196
|
-
|
|
197
|
-
**N+1 Problem:**
|
|
198
|
-
```typescript
|
|
199
|
-
// BAD: N+1 queries
|
|
200
|
-
const orders = await orderRepo.findByCustomer(customerId);
|
|
201
|
-
const itemsPerOrder = await Promise.all(
|
|
202
|
-
orders.map(o => itemRepo.findByOrder(o.id)) // N queries
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
// GOOD: Single query with denormalization
|
|
206
|
-
const orderData = await db.orders
|
|
207
|
-
.where({ customerId })
|
|
208
|
-
.select(['*', 'items']) // Includes denormalized items
|
|
209
|
-
.sort({ createdAt: 'DESC' });
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
**Index Coverage:**
|
|
213
|
-
```sql
|
|
214
|
-
-- Bad: Full table scan
|
|
215
|
-
SELECT * FROM orders WHERE customer_id = $1;
|
|
216
|
-
|
|
217
|
-
-- Good: Index-covered query
|
|
218
|
-
CREATE INDEX idx_orders_customer_status
|
|
219
|
-
ON orders(customer_id, status);
|
|
220
|
-
|
|
221
|
-
SELECT * FROM orders
|
|
222
|
-
WHERE customer_id = $1 AND status = 'pending';
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
## Data Transformation Patterns
|
|
226
|
-
|
|
227
|
-
### Request → Entity → Record → Response Flow
|
|
228
|
-
|
|
229
|
-
```typescript
|
|
230
|
-
// 1. HTTP Request arrives
|
|
231
|
-
POST /orders
|
|
232
|
-
{
|
|
233
|
-
"customerId": "cust-123",
|
|
234
|
-
"items": [{ "productId": "prod-1", "quantity": 2 }]
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// 2. Router parses to request type
|
|
238
|
-
const request: CreateOrderRequest = req.body;
|
|
239
|
-
|
|
240
|
-
// 3. Entity transforms from request (with ID generation)
|
|
241
|
-
const order = Order.fromRequest(request);
|
|
242
|
-
|
|
243
|
-
// 4. Entity validates business rules
|
|
244
|
-
const validation = order.validate();
|
|
245
|
-
if (!validation.ok) return error;
|
|
246
|
-
|
|
247
|
-
// 5. Service performs logic, uses repository
|
|
248
|
-
await repo.save(order.toRecord());
|
|
249
|
-
|
|
250
|
-
// 6. Repository converts to database record
|
|
251
|
-
{
|
|
252
|
-
id: "order-abc",
|
|
253
|
-
customer_id: "cust-123",
|
|
254
|
-
items: "[...]",
|
|
255
|
-
status: "pending",
|
|
256
|
-
created_at: "2024-01-21T10:00:00Z"
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// 7. Response converts from record
|
|
260
|
-
const response = order.toResponse();
|
|
261
|
-
|
|
262
|
-
// 8. HTTP Response sends
|
|
263
|
-
{
|
|
264
|
-
"id": "order-abc",
|
|
265
|
-
"customerId": "cust-123",
|
|
266
|
-
"items": [...],
|
|
267
|
-
"status": "pending"
|
|
268
|
-
}
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
### Transformation Considerations
|
|
272
|
-
|
|
273
|
-
**Immutability:**
|
|
274
|
-
```typescript
|
|
275
|
-
// Bad: Mutation during transform
|
|
276
|
-
toRecord(): OrderRecord {
|
|
277
|
-
this.items.forEach(i => i.status = 'recorded');
|
|
278
|
-
return { items: this.items };
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Good: No side effects
|
|
282
|
-
toRecord(): OrderRecord {
|
|
283
|
-
return {
|
|
284
|
-
items: this.items.map(i => ({
|
|
285
|
-
...i,
|
|
286
|
-
// Transform properties as needed
|
|
287
|
-
}))
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
**Versioning:**
|
|
293
|
-
```typescript
|
|
294
|
-
// Handle multiple API versions
|
|
295
|
-
toResponse(version: 'v1' | 'v2'): OrderResponse {
|
|
296
|
-
const base = {
|
|
297
|
-
id: this.id,
|
|
298
|
-
customerId: this.customerId,
|
|
299
|
-
status: this.status
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
if (version === 'v2') {
|
|
303
|
-
return { ...base, total: this.total };
|
|
304
|
-
}
|
|
305
|
-
return base;
|
|
306
|
-
}
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
**Partial Loading:**
|
|
310
|
-
```typescript
|
|
311
|
-
// Only load fields needed for the operation
|
|
312
|
-
async findOrderSummary(id: string): Promise<OrderSummary> {
|
|
313
|
-
const record = await db.orders
|
|
314
|
-
.select(['id', 'status', 'total'])
|
|
315
|
-
.findOne({ id });
|
|
316
|
-
return { id: record.id, status: record.status, total: record.total };
|
|
317
|
-
}
|
|
318
|
-
```
|
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
# DDD Patterns Reference
|
|
2
|
-
|
|
3
|
-
Domain-Driven Design patterns for modeling business domains and enforcing consistency.
|
|
4
|
-
|
|
5
|
-
## Aggregates
|
|
6
|
-
|
|
7
|
-
**Definition:** Cluster of domain objects treated as a single unit for data changes
|
|
8
|
-
|
|
9
|
-
**Rules:**
|
|
10
|
-
- One entity is the aggregate root (e.g., Order)
|
|
11
|
-
- External references only to the root
|
|
12
|
-
- Changes go through the root
|
|
13
|
-
- Root enforces invariants
|
|
14
|
-
|
|
15
|
-
**Example:**
|
|
16
|
-
```typescript
|
|
17
|
-
class Order { // Aggregate root
|
|
18
|
-
private items: OrderItem[]; // Part of aggregate
|
|
19
|
-
|
|
20
|
-
addItem(item: OrderItem): Result<Order> {
|
|
21
|
-
// Root enforces invariants
|
|
22
|
-
if (this.items.length >= 100) {
|
|
23
|
-
return Err('Order cannot have more than 100 items');
|
|
24
|
-
}
|
|
25
|
-
this.items.push(item);
|
|
26
|
-
return Ok(this);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
**Business Rules Enforcement:**
|
|
32
|
-
```typescript
|
|
33
|
-
class Order {
|
|
34
|
-
private status: OrderStatus;
|
|
35
|
-
private appliedDiscounts: Discount[] = [];
|
|
36
|
-
|
|
37
|
-
applyDiscount(discount: Discount): Result<void> {
|
|
38
|
-
// Business rule: can't apply discount to shipped orders
|
|
39
|
-
if (this.status === 'shipped') {
|
|
40
|
-
return Err('Cannot discount shipped orders');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Business rule: no duplicate discounts
|
|
44
|
-
if (this.appliedDiscounts.some(d => d.id === discount.id)) {
|
|
45
|
-
return Err('Discount already applied');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
this.appliedDiscounts.push(discount);
|
|
49
|
-
return Ok(undefined);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Value Objects
|
|
55
|
-
|
|
56
|
-
**Definition:** Immutable objects defined by their attributes, not identity
|
|
57
|
-
|
|
58
|
-
**Characteristics:**
|
|
59
|
-
- No identity (equality by value)
|
|
60
|
-
- Immutable
|
|
61
|
-
- Self-validating
|
|
62
|
-
- Encapsulate validation logic
|
|
63
|
-
|
|
64
|
-
**Example:**
|
|
65
|
-
```typescript
|
|
66
|
-
class Money {
|
|
67
|
-
constructor(
|
|
68
|
-
public readonly amount: number,
|
|
69
|
-
public readonly currency: string
|
|
70
|
-
) {
|
|
71
|
-
if (amount < 0) throw new Error('Amount cannot be negative');
|
|
72
|
-
if (!['USD', 'EUR', 'GBP'].includes(currency)) {
|
|
73
|
-
throw new Error('Unsupported currency');
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
add(other: Money): Money {
|
|
78
|
-
if (this.currency !== other.currency) {
|
|
79
|
-
throw new Error('Cannot add different currencies');
|
|
80
|
-
}
|
|
81
|
-
return new Money(this.amount + other.amount, this.currency);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
equals(other: Money): boolean {
|
|
85
|
-
return this.amount === other.amount && this.currency === other.currency;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
static fromCents(cents: number, currency: string): Money {
|
|
89
|
-
return new Money(cents / 100, currency);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
**More Examples:**
|
|
95
|
-
```typescript
|
|
96
|
-
// Email VO with validation
|
|
97
|
-
class Email {
|
|
98
|
-
readonly value: string;
|
|
99
|
-
|
|
100
|
-
constructor(value: string) {
|
|
101
|
-
if (!this.isValid(value)) throw new Error('Invalid email');
|
|
102
|
-
this.value = value.toLowerCase();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
private isValid(email: string): boolean {
|
|
106
|
-
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
equals(other: Email): boolean {
|
|
110
|
-
return this.value === other.value;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Address VO with composition
|
|
115
|
-
class Address {
|
|
116
|
-
constructor(
|
|
117
|
-
public readonly street: string,
|
|
118
|
-
public readonly city: string,
|
|
119
|
-
public readonly zipCode: string,
|
|
120
|
-
public readonly country: string
|
|
121
|
-
) {}
|
|
122
|
-
|
|
123
|
-
equals(other: Address): boolean {
|
|
124
|
-
return this.street === other.street &&
|
|
125
|
-
this.city === other.city &&
|
|
126
|
-
this.zipCode === other.zipCode &&
|
|
127
|
-
this.country === other.country;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
## Domain Events
|
|
133
|
-
|
|
134
|
-
**Definition:** Something that happened in the domain that domain experts care about
|
|
135
|
-
|
|
136
|
-
**Characteristics:**
|
|
137
|
-
- Immutable facts about what happened
|
|
138
|
-
- Named in past tense
|
|
139
|
-
- Include context about what changed
|
|
140
|
-
- Timestamped
|
|
141
|
-
|
|
142
|
-
**Example:**
|
|
143
|
-
```typescript
|
|
144
|
-
class OrderPlaced {
|
|
145
|
-
constructor(
|
|
146
|
-
public readonly orderId: string,
|
|
147
|
-
public readonly customerId: string,
|
|
148
|
-
public readonly total: number,
|
|
149
|
-
public readonly occurredAt: Date
|
|
150
|
-
) {}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Service publishes events
|
|
154
|
-
const order = await orderService.createOrder(request);
|
|
155
|
-
await eventBus.publish(
|
|
156
|
-
new OrderPlaced(order.id, order.customerId, order.total, new Date())
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
// Events cross bounded contexts
|
|
160
|
-
class OrderShipped {
|
|
161
|
-
constructor(
|
|
162
|
-
public readonly orderId: string,
|
|
163
|
-
public readonly shippingAddress: Address,
|
|
164
|
-
public readonly trackingNumber: string,
|
|
165
|
-
public readonly occurredAt: Date
|
|
166
|
-
) {}
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
**Event-Driven Workflows:**
|
|
171
|
-
```typescript
|
|
172
|
-
// Order context publishes OrderPlaced
|
|
173
|
-
// Inventory context listens and consumes
|
|
174
|
-
class InventoryService {
|
|
175
|
-
async handleOrderPlaced(event: OrderPlaced): Promise<Result<void>> {
|
|
176
|
-
// Reserve inventory in response to order
|
|
177
|
-
const result = await this.inventoryRepo.reserve(event.orderId);
|
|
178
|
-
if (!result.ok) {
|
|
179
|
-
// Handle reservation failure
|
|
180
|
-
await this.eventBus.publish(new InventoryReservationFailed(event.orderId));
|
|
181
|
-
}
|
|
182
|
-
return result;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
## Bounded Contexts
|
|
188
|
-
|
|
189
|
-
**Definition:** Explicit boundaries where a domain model applies
|
|
190
|
-
|
|
191
|
-
**Guidelines:**
|
|
192
|
-
- Each context has its own ubiquitous language
|
|
193
|
-
- Contexts communicate via well-defined contracts
|
|
194
|
-
- Same concept may have different models in different contexts
|
|
195
|
-
- Example: "Customer" in Sales context vs "User" in Auth context
|
|
196
|
-
|
|
197
|
-
**Context Composition:**
|
|
198
|
-
```
|
|
199
|
-
Bounded Context: "Orders"
|
|
200
|
-
┌────────────────────────────────────────────────────────────────┐
|
|
201
|
-
│ │
|
|
202
|
-
│ Effectful Edge Functional Core │
|
|
203
|
-
│ ┌──────────────────┐ ┌──────────────────┐ │
|
|
204
|
-
│ │ Router │────────────▶│ Service │ │
|
|
205
|
-
│ │ POST /orders │ │ OrderService │ │
|
|
206
|
-
│ └──────────────────┘ └────────┬─────────┘ │
|
|
207
|
-
│ │ │
|
|
208
|
-
│ ┌──────────────────┐ ┌────────▼─────────┐ │
|
|
209
|
-
│ │ Repository │◀────────────│ Aggregate │ │
|
|
210
|
-
│ │ OrderRepository │ │ Order (root) │ │
|
|
211
|
-
│ └──────────────────┘ │ └─ OrderItem[] │ │
|
|
212
|
-
│ │ └─ Money (VO) │ │
|
|
213
|
-
│ ┌──────────────────┐ └──────────────────┘ │
|
|
214
|
-
│ │ Producer │◀── Domain Event: OrderPlaced │
|
|
215
|
-
│ └────────┬─────────┘ │
|
|
216
|
-
└────────────│───────────────────────────────────────────────────┘
|
|
217
|
-
│
|
|
218
|
-
▼ Events cross context boundaries
|
|
219
|
-
┌────────────────────────────────────────────────────────────────┐
|
|
220
|
-
│ Bounded Context: "Inventory" │
|
|
221
|
-
│ Consumer ──▶ InventoryService ──▶ StockLevel (Aggregate) │
|
|
222
|
-
└────────────────────────────────────────────────────────────────┘
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
**Context Anti-Patterns:**
|
|
226
|
-
- Shared database (breaks boundary enforcement)
|
|
227
|
-
- Generic "core" or "common" domain (pollutes context boundaries)
|
|
228
|
-
- Cross-context validation (move validation to appropriate context)
|
|
229
|
-
- Tight coupling via shared types (use context-specific DTOs)
|
|
230
|
-
|
|
231
|
-
## Composition Rules
|
|
232
|
-
|
|
233
|
-
| DDD Pattern | Maps To | Location |
|
|
234
|
-
|-------------|---------|----------|
|
|
235
|
-
| Bounded Context | Module/Package boundary | Contains all layers |
|
|
236
|
-
| Aggregate | Entity cluster | Functional Core |
|
|
237
|
-
| Aggregate Root | Primary Entity | Entity layer |
|
|
238
|
-
| Value Object | Immutable type | Entity layer |
|
|
239
|
-
| Domain Event | Event class | Published from Service, consumed by edge |
|
|
240
|
-
| Repository | Data access | Effectful Edge (interface in Core) |
|
|
241
|
-
|
|
242
|
-
**Key Interactions:**
|
|
243
|
-
1. **Router → Service**: HTTP request parsed, passed to service
|
|
244
|
-
2. **Service → Aggregate**: Service orchestrates aggregate operations
|
|
245
|
-
3. **Aggregate → Repository**: Service uses repository to persist aggregate
|
|
246
|
-
4. **Service → Producer**: Service publishes domain events after state change
|
|
247
|
-
5. **Consumer → Service**: Events from other contexts trigger service operations
|
|
248
|
-
|
|
249
|
-
## Seam Placement
|
|
250
|
-
|
|
251
|
-
Where you put the seam matters as much as what goes behind it.
|
|
252
|
-
|
|
253
|
-
### Core/Edge Seam
|
|
254
|
-
|
|
255
|
-
The primary seam runs between the functional core and the effectful edge:
|
|
256
|
-
|
|
257
|
-
```
|
|
258
|
-
┌─────────────────────────────────┐
|
|
259
|
-
│ Functional Core (pure) │
|
|
260
|
-
│ ┌──────────────────────────┐ │
|
|
261
|
-
│ │ OrderService │ │
|
|
262
|
-
│ │ createOrder() ────────┼───┼──▶ (seam)
|
|
263
|
-
│ │ processPayment() │ │
|
|
264
|
-
│ └──────────────────────────┘ │
|
|
265
|
-
└─────────────────────────────────┘
|
|
266
|
-
│
|
|
267
|
-
▼ seam
|
|
268
|
-
┌─────────────────────────────────┐
|
|
269
|
-
│ Effectful Edge (IO) │
|
|
270
|
-
│ ┌──────────────────────────┐ │
|
|
271
|
-
│ │ PostgresOrderRepository │◀──┼── adapter
|
|
272
|
-
│ │ HttpPaymentClient │ │ adapter
|
|
273
|
-
│ │ InMemoryOrderRepository │ │ adapter (test)
|
|
274
|
-
│ └──────────────────────────┘ │
|
|
275
|
-
└─────────────────────────────────┘
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
**Rules:**
|
|
279
|
-
- Core defines the interface; edge implements it (dependency inversion)
|
|
280
|
-
- The seam must have **at least two adapters** to be real (production + test)
|
|
281
|
-
- Core never imports edge code; edge imports core interfaces
|
|
282
|
-
|
|
283
|
-
### Internal Seams
|
|
284
|
-
|
|
285
|
-
A deep module can have **internal seams** — private to its implementation, used by its own tests:
|
|
286
|
-
|
|
287
|
-
```typescript
|
|
288
|
-
class Order {
|
|
289
|
-
// External seam (public interface)
|
|
290
|
-
validate(): Result<Order> { ... }
|
|
291
|
-
|
|
292
|
-
// Internal seam (private, tested directly)
|
|
293
|
-
private calculateTax(subtotal: number): Money { ... }
|
|
294
|
-
}
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
Internal seams let you test complex logic without exposing it in the public interface.
|
|
298
|
-
|
|
299
|
-
### When to Create a Seam
|
|
300
|
-
|
|
301
|
-
Use the **adapter rule**: one adapter = hypothetical; two = real.
|
|
302
|
-
|
|
303
|
-
| Scenario | Action |
|
|
304
|
-
|----------|--------|
|
|
305
|
-
| One database implementation, no test fake | No seam needed. Direct dependency is simpler. |
|
|
306
|
-
| One database implementation, test fake exists | Extract interface. The fake is the second adapter. |
|
|
307
|
-
| Multiple database implementations (Postgres + DynamoDB) | Extract interface. The implementations are the adapters. |
|
|
308
|
-
| External API with one client | Direct dependency. Introduce seam only when you need a mock/stub for testing. |
|
|
309
|
-
|
|
310
|
-
### Anti-Patterns
|
|
311
|
-
|
|
312
|
-
**Seam at every dependency:**
|
|
313
|
-
```typescript
|
|
314
|
-
// ❌ Too many seams — every dependency gets an interface
|
|
315
|
-
interface IValidator { ... }
|
|
316
|
-
interface IFormatter { ... }
|
|
317
|
-
interface IConverter { ... }
|
|
318
|
-
// Each has one adapter. Indirection without value.
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
**No seam at the core/edge boundary:**
|
|
322
|
-
```typescript
|
|
323
|
-
// ❌ Core imports edge directly
|
|
324
|
-
import { db } from '../edge/postgres';
|
|
325
|
-
class OrderService {
|
|
326
|
-
async createOrder(req) {
|
|
327
|
-
await db.orders.create(...); // Core knows about Postgres
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
**Leaky seam:**
|
|
333
|
-
```typescript
|
|
334
|
-
// ❌ Interface exposes implementation details
|
|
335
|
-
interface OrderRepository {
|
|
336
|
-
queryRaw(sql: string): Promise<Row[]>; // Caller knows SQL
|
|
337
|
-
beginTransaction(): Promise<Transaction>; // Caller manages transactions
|
|
338
|
-
}
|
|
339
|
-
// The seam leaks. Callers become coupled to the database.
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
### Seam Placement Checklist
|
|
343
|
-
|
|
344
|
-
Before introducing a new seam, verify:
|
|
345
|
-
- [ ] At least two adapters will exist (production + test, or multiple production)
|
|
346
|
-
- [ ] The interface hides implementation details (no SQL, no HTTP, no framework types)
|
|
347
|
-
- [ ] The core doesn't import edge code
|
|
348
|
-
- [ ] Deleting the module would re-appear complexity across N callers
|