@martinffx/atelier 2.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +94 -253
- package/package.json +1 -1
- package/skills/{spec-orchestrator → atelier-orchestrator}/SKILL.md +3 -3
- package/skills/atelier-setup/SKILL.md +80 -0
- package/skills/code-review/references/reviewers.md +3 -3
- package/skills/code-review/references/rq.md +6 -6
- package/skills/spec-brainstorm/SKILL.md +5 -5
- package/skills/spec-implement/SKILL.md +2 -3
- package/skills/spec-plan/SKILL.md +21 -29
- package/skills/spec-plan/references/plan_template.md +77 -0
- package/skills/python-architecture/SKILL.md +0 -148
- package/skills/python-architecture/references/data-modeling.md +0 -558
- package/skills/python-architecture/references/ddd.md +0 -569
- package/skills/python-architecture/references/functional-core.md +0 -337
- package/skills/python-build-tools/SKILL.md +0 -153
- package/skills/python-build-tools/references/basedpyright.md +0 -321
- package/skills/python-build-tools/references/ruff.md +0 -225
- package/skills/python-build-tools/references/uv.md +0 -257
- package/skills/python-fastapi/SKILL.md +0 -184
- package/skills/python-fastapi/references/api-design.md +0 -563
- package/skills/python-fastapi/references/dependencies.md +0 -479
- package/skills/python-fastapi/references/middleware.md +0 -475
- package/skills/python-fastapi/references/validation.md +0 -497
- package/skills/python-modern-python/SKILL.md +0 -201
- package/skills/python-modern-python/references/async.md +0 -76
- package/skills/python-modern-python/references/pattern-matching.md +0 -89
- package/skills/python-modern-python/references/typing.md +0 -96
- package/skills/python-monorepo/SKILL.md +0 -146
- package/skills/python-monorepo/references/docker.md +0 -515
- package/skills/python-monorepo/references/namespace-packages.md +0 -426
- package/skills/python-monorepo/references/workspace-config.md +0 -341
- package/skills/python-sqlalchemy/SKILL.md +0 -119
- package/skills/python-sqlalchemy/references/async.md +0 -51
- package/skills/python-sqlalchemy/references/models.md +0 -67
- package/skills/python-sqlalchemy/references/queries.md +0 -57
- package/skills/python-temporal/SKILL.md +0 -128
- package/skills/python-temporal/references/patterns.md +0 -87
- package/skills/python-temporal/references/testing.md +0 -54
- package/skills/python-testing/SKILL.md +0 -161
- package/skills/python-testing/references/boundaries.md +0 -449
- package/skills/python-testing/references/mocking.md +0 -449
- package/skills/python-testing/references/pytest.md +0 -524
- package/skills/typescript-api-design/SKILL.md +0 -396
- package/skills/typescript-api-design/references/error-responses.md +0 -469
- package/skills/typescript-build-tools/SKILL.md +0 -447
- package/skills/typescript-build-tools/references/biome.md +0 -436
- package/skills/typescript-build-tools/references/bun.md +0 -432
- package/skills/typescript-build-tools/references/turborepo.md +0 -426
- package/skills/typescript-build-tools/references/vitest.md +0 -518
- package/skills/typescript-drizzle-orm/SKILL.md +0 -398
- package/skills/typescript-drizzle-orm/references/cloudflare.md +0 -392
- package/skills/typescript-drizzle-orm/references/entity-pattern.md +0 -412
- package/skills/typescript-drizzle-orm/references/postgresql.md +0 -191
- package/skills/typescript-drizzle-orm/references/repository-pattern.md +0 -479
- package/skills/typescript-drizzle-orm/references/sqlite.md +0 -291
- package/skills/typescript-dynamodb-toolbox/SKILL.md +0 -370
- package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +0 -574
- package/skills/typescript-dynamodb-toolbox/references/error-handling.md +0 -495
- package/skills/typescript-dynamodb-toolbox/references/modeling.md +0 -332
- package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +0 -631
- package/skills/typescript-dynamodb-toolbox/references/testing.md +0 -718
- package/skills/typescript-dynamodb-toolbox/references/transactions.md +0 -423
- package/skills/typescript-effect-ts/SKILL.md +0 -341
- package/skills/typescript-effect-ts/references/core.md +0 -412
- package/skills/typescript-effect-ts/references/error-handling.md +0 -468
- package/skills/typescript-effect-ts/references/resources.md +0 -476
- package/skills/typescript-effect-ts/references/schema.md +0 -536
- package/skills/typescript-effect-ts/references/services.md +0 -461
- package/skills/typescript-fastify/SKILL.md +0 -405
- package/skills/typescript-fastify/references/plugins.md +0 -329
- package/skills/typescript-fastify/references/typeid.md +0 -315
- package/skills/typescript-functional-patterns/SKILL.md +0 -402
- package/skills/typescript-functional-patterns/references/adts.md +0 -497
- package/skills/typescript-functional-patterns/references/branded-types.md +0 -542
- package/skills/typescript-functional-patterns/references/migration-guide.md +0 -695
- package/skills/typescript-functional-patterns/references/option-result.md +0 -585
- package/skills/typescript-testing/SKILL.md +0 -353
- package/skills/typescript-testing/references/mocking.md +0 -675
- package/skills/typescript-testing/references/msw.md +0 -852
- package/skills/typescript-testing/references/snapshot-testing.md +0 -774
|
@@ -1,569 +0,0 @@
|
|
|
1
|
-
# Domain-Driven Design in Python
|
|
2
|
-
|
|
3
|
-
Domain-Driven Design (DDD) provides patterns for modeling complex business domains and maintaining clean separation of concerns.
|
|
4
|
-
|
|
5
|
-
## Core Building Blocks
|
|
6
|
-
|
|
7
|
-
### Entities
|
|
8
|
-
|
|
9
|
-
Objects with identity that persists over time.
|
|
10
|
-
|
|
11
|
-
**Characteristics:**
|
|
12
|
-
- Has unique identifier (ID)
|
|
13
|
-
- Mutable state
|
|
14
|
-
- Equality based on ID, not attributes
|
|
15
|
-
- Encapsulates business behavior
|
|
16
|
-
|
|
17
|
-
```python
|
|
18
|
-
from dataclasses import dataclass
|
|
19
|
-
from uuid import UUID, uuid4
|
|
20
|
-
from decimal import Decimal
|
|
21
|
-
|
|
22
|
-
@dataclass
|
|
23
|
-
class Account:
|
|
24
|
-
"""Entity - has identity"""
|
|
25
|
-
id: UUID
|
|
26
|
-
owner_id: UUID
|
|
27
|
-
balance: Decimal
|
|
28
|
-
|
|
29
|
-
def __eq__(self, other: object) -> bool:
|
|
30
|
-
"""Equality based on ID"""
|
|
31
|
-
if not isinstance(other, Account):
|
|
32
|
-
return False
|
|
33
|
-
return self.id == other.id
|
|
34
|
-
|
|
35
|
-
def __hash__(self) -> int:
|
|
36
|
-
"""Hash based on ID"""
|
|
37
|
-
return hash(self.id)
|
|
38
|
-
|
|
39
|
-
def deposit(self, amount: Decimal) -> None:
|
|
40
|
-
"""Business behavior encapsulated in entity"""
|
|
41
|
-
if amount <= 0:
|
|
42
|
-
raise ValueError("Deposit amount must be positive")
|
|
43
|
-
self.balance += amount
|
|
44
|
-
|
|
45
|
-
def withdraw(self, amount: Decimal) -> None:
|
|
46
|
-
"""Enforce business rules"""
|
|
47
|
-
if amount <= 0:
|
|
48
|
-
raise ValueError("Withdrawal amount must be positive")
|
|
49
|
-
if amount > self.balance:
|
|
50
|
-
raise InsufficientFundsError(f"Cannot withdraw {amount} from balance {self.balance}")
|
|
51
|
-
self.balance -= amount
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### Value Objects
|
|
55
|
-
|
|
56
|
-
Objects defined entirely by their attributes.
|
|
57
|
-
|
|
58
|
-
**Characteristics:**
|
|
59
|
-
- No identity
|
|
60
|
-
- Immutable
|
|
61
|
-
- Equality based on all attributes
|
|
62
|
-
- Interchangeable
|
|
63
|
-
|
|
64
|
-
```python
|
|
65
|
-
from dataclasses import dataclass
|
|
66
|
-
from decimal import Decimal
|
|
67
|
-
|
|
68
|
-
@dataclass(frozen=True)
|
|
69
|
-
class Money:
|
|
70
|
-
"""Value object - immutable, no identity"""
|
|
71
|
-
amount: Decimal
|
|
72
|
-
currency: str
|
|
73
|
-
|
|
74
|
-
def __post_init__(self):
|
|
75
|
-
"""Validate on construction"""
|
|
76
|
-
if self.amount < 0:
|
|
77
|
-
raise ValueError("Money amount cannot be negative")
|
|
78
|
-
if not self.currency:
|
|
79
|
-
raise ValueError("Currency is required")
|
|
80
|
-
|
|
81
|
-
def add(self, other: "Money") -> "Money":
|
|
82
|
-
"""Return new instance instead of mutating"""
|
|
83
|
-
if self.currency != other.currency:
|
|
84
|
-
raise ValueError(f"Cannot add {other.currency} to {self.currency}")
|
|
85
|
-
return Money(self.amount + other.amount, self.currency)
|
|
86
|
-
|
|
87
|
-
def multiply(self, factor: Decimal) -> "Money":
|
|
88
|
-
"""All operations return new instances"""
|
|
89
|
-
return Money(self.amount * factor, self.currency)
|
|
90
|
-
|
|
91
|
-
@dataclass(frozen=True)
|
|
92
|
-
class Address:
|
|
93
|
-
"""Value object for address"""
|
|
94
|
-
street: str
|
|
95
|
-
city: str
|
|
96
|
-
state: str
|
|
97
|
-
zip_code: str
|
|
98
|
-
|
|
99
|
-
def __post_init__(self):
|
|
100
|
-
"""Validation ensures value object is always valid"""
|
|
101
|
-
if not self.street or not self.city:
|
|
102
|
-
raise ValueError("Street and city are required")
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
**When to use Entity vs Value Object:**
|
|
106
|
-
- Entity: Need to track it over time (User, Order, Product)
|
|
107
|
-
- Value Object: Just a measurement or description (Money, Address, DateRange)
|
|
108
|
-
|
|
109
|
-
### Aggregates
|
|
110
|
-
|
|
111
|
-
Cluster of entities and value objects treated as a unit.
|
|
112
|
-
|
|
113
|
-
**Characteristics:**
|
|
114
|
-
- Has aggregate root (entry point entity)
|
|
115
|
-
- Transaction boundary
|
|
116
|
-
- Enforces invariants
|
|
117
|
-
- Internal entities accessed only through root
|
|
118
|
-
|
|
119
|
-
```python
|
|
120
|
-
from dataclasses import dataclass, field
|
|
121
|
-
from uuid import UUID, uuid4
|
|
122
|
-
from decimal import Decimal
|
|
123
|
-
|
|
124
|
-
@dataclass
|
|
125
|
-
class OrderLine:
|
|
126
|
-
"""Entity inside aggregate - not aggregate root"""
|
|
127
|
-
id: UUID
|
|
128
|
-
product_id: UUID
|
|
129
|
-
quantity: int
|
|
130
|
-
unit_price: Money
|
|
131
|
-
|
|
132
|
-
def subtotal(self) -> Money:
|
|
133
|
-
return self.unit_price.multiply(Decimal(self.quantity))
|
|
134
|
-
|
|
135
|
-
@dataclass
|
|
136
|
-
class Order:
|
|
137
|
-
"""Aggregate root - controls access to OrderLines"""
|
|
138
|
-
id: UUID
|
|
139
|
-
customer_id: UUID
|
|
140
|
-
lines: list[OrderLine] = field(default_factory=list)
|
|
141
|
-
status: str = "draft"
|
|
142
|
-
|
|
143
|
-
def add_line(self, product_id: UUID, quantity: int, unit_price: Money) -> None:
|
|
144
|
-
"""Control access through aggregate root"""
|
|
145
|
-
if quantity <= 0:
|
|
146
|
-
raise ValueError("Quantity must be positive")
|
|
147
|
-
|
|
148
|
-
# Check if product already in order
|
|
149
|
-
for line in self.lines:
|
|
150
|
-
if line.product_id == product_id:
|
|
151
|
-
raise ValueError("Product already in order")
|
|
152
|
-
|
|
153
|
-
line = OrderLine(
|
|
154
|
-
id=uuid4(),
|
|
155
|
-
product_id=product_id,
|
|
156
|
-
quantity=quantity,
|
|
157
|
-
unit_price=unit_price
|
|
158
|
-
)
|
|
159
|
-
self.lines.append(line)
|
|
160
|
-
|
|
161
|
-
def remove_line(self, line_id: UUID) -> None:
|
|
162
|
-
"""Aggregate root enforces invariants"""
|
|
163
|
-
self.lines = [line for line in self.lines if line.id != line_id]
|
|
164
|
-
|
|
165
|
-
def total(self) -> Money:
|
|
166
|
-
"""Calculated from all lines"""
|
|
167
|
-
if not self.lines:
|
|
168
|
-
return Money(Decimal("0"), "USD")
|
|
169
|
-
|
|
170
|
-
total = self.lines[0].subtotal()
|
|
171
|
-
for line in self.lines[1:]:
|
|
172
|
-
total = total.add(line.subtotal())
|
|
173
|
-
return total
|
|
174
|
-
|
|
175
|
-
def submit(self) -> None:
|
|
176
|
-
"""State transition with validation"""
|
|
177
|
-
if self.status != "draft":
|
|
178
|
-
raise ValueError("Only draft orders can be submitted")
|
|
179
|
-
if not self.lines:
|
|
180
|
-
raise ValueError("Cannot submit empty order")
|
|
181
|
-
self.status = "submitted"
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
**Aggregate Design Rules:**
|
|
185
|
-
1. Keep aggregates small (minimize entities inside)
|
|
186
|
-
2. Reference other aggregates by ID only
|
|
187
|
-
3. Update one aggregate per transaction
|
|
188
|
-
4. Use eventual consistency between aggregates
|
|
189
|
-
|
|
190
|
-
### Repository Pattern
|
|
191
|
-
|
|
192
|
-
Abstraction for storage and retrieval of aggregates.
|
|
193
|
-
|
|
194
|
-
**Characteristics:**
|
|
195
|
-
- Collection-like interface
|
|
196
|
-
- Hides persistence details
|
|
197
|
-
- Works with aggregate roots only
|
|
198
|
-
- Returns domain objects, not records
|
|
199
|
-
|
|
200
|
-
```python
|
|
201
|
-
from abc import ABC, abstractmethod
|
|
202
|
-
from typing import Optional
|
|
203
|
-
from uuid import UUID
|
|
204
|
-
|
|
205
|
-
class OrderRepository(ABC):
|
|
206
|
-
"""Abstract repository - defines interface"""
|
|
207
|
-
|
|
208
|
-
@abstractmethod
|
|
209
|
-
def get(self, order_id: UUID) -> Optional[Order]:
|
|
210
|
-
"""Fetch by ID"""
|
|
211
|
-
pass
|
|
212
|
-
|
|
213
|
-
@abstractmethod
|
|
214
|
-
def save(self, order: Order) -> None:
|
|
215
|
-
"""Save aggregate"""
|
|
216
|
-
pass
|
|
217
|
-
|
|
218
|
-
@abstractmethod
|
|
219
|
-
def delete(self, order_id: UUID) -> None:
|
|
220
|
-
"""Remove aggregate"""
|
|
221
|
-
pass
|
|
222
|
-
|
|
223
|
-
@abstractmethod
|
|
224
|
-
def find_by_customer(self, customer_id: UUID) -> list[Order]:
|
|
225
|
-
"""Query method"""
|
|
226
|
-
pass
|
|
227
|
-
|
|
228
|
-
@abstractmethod
|
|
229
|
-
def find_by_status(self, status: str) -> list[Order]:
|
|
230
|
-
"""Another query method"""
|
|
231
|
-
pass
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
**Implementation with SQLAlchemy:**
|
|
235
|
-
|
|
236
|
-
```python
|
|
237
|
-
from sqlalchemy.orm import Session
|
|
238
|
-
|
|
239
|
-
class SqlAlchemyOrderRepository(OrderRepository):
|
|
240
|
-
"""Concrete implementation"""
|
|
241
|
-
|
|
242
|
-
def __init__(self, session: Session):
|
|
243
|
-
self._session = session
|
|
244
|
-
|
|
245
|
-
def get(self, order_id: UUID) -> Optional[Order]:
|
|
246
|
-
"""Fetch and convert to domain object"""
|
|
247
|
-
record = self._session.get(OrderRecord, order_id)
|
|
248
|
-
if not record:
|
|
249
|
-
return None
|
|
250
|
-
return self._to_domain(record)
|
|
251
|
-
|
|
252
|
-
def save(self, order: Order) -> None:
|
|
253
|
-
"""Convert to record and save"""
|
|
254
|
-
record = self._to_record(order)
|
|
255
|
-
self._session.merge(record)
|
|
256
|
-
self._session.commit()
|
|
257
|
-
|
|
258
|
-
def delete(self, order_id: UUID) -> None:
|
|
259
|
-
record = self._session.get(OrderRecord, order_id)
|
|
260
|
-
if record:
|
|
261
|
-
self._session.delete(record)
|
|
262
|
-
self._session.commit()
|
|
263
|
-
|
|
264
|
-
def find_by_customer(self, customer_id: UUID) -> list[Order]:
|
|
265
|
-
records = self._session.query(OrderRecord).filter_by(customer_id=customer_id).all()
|
|
266
|
-
return [self._to_domain(r) for r in records]
|
|
267
|
-
|
|
268
|
-
def find_by_status(self, status: str) -> list[Order]:
|
|
269
|
-
records = self._session.query(OrderRecord).filter_by(status=status).all()
|
|
270
|
-
return [self._to_domain(r) for r in records]
|
|
271
|
-
|
|
272
|
-
def _to_domain(self, record: OrderRecord) -> Order:
|
|
273
|
-
"""Convert database record to domain object"""
|
|
274
|
-
lines = [
|
|
275
|
-
OrderLine(
|
|
276
|
-
id=line.id,
|
|
277
|
-
product_id=line.product_id,
|
|
278
|
-
quantity=line.quantity,
|
|
279
|
-
unit_price=Money(line.unit_price, line.currency)
|
|
280
|
-
)
|
|
281
|
-
for line in record.lines
|
|
282
|
-
]
|
|
283
|
-
return Order(
|
|
284
|
-
id=record.id,
|
|
285
|
-
customer_id=record.customer_id,
|
|
286
|
-
lines=lines,
|
|
287
|
-
status=record.status
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
def _to_record(self, order: Order) -> OrderRecord:
|
|
291
|
-
"""Convert domain object to database record"""
|
|
292
|
-
return OrderRecord(
|
|
293
|
-
id=order.id,
|
|
294
|
-
customer_id=order.customer_id,
|
|
295
|
-
status=order.status,
|
|
296
|
-
lines=[
|
|
297
|
-
OrderLineRecord(
|
|
298
|
-
id=line.id,
|
|
299
|
-
product_id=line.product_id,
|
|
300
|
-
quantity=line.quantity,
|
|
301
|
-
unit_price=line.unit_price.amount,
|
|
302
|
-
currency=line.unit_price.currency
|
|
303
|
-
)
|
|
304
|
-
for line in order.lines
|
|
305
|
-
]
|
|
306
|
-
)
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### Domain Services
|
|
310
|
-
|
|
311
|
-
Business logic that doesn't naturally fit in an entity.
|
|
312
|
-
|
|
313
|
-
**Use when:**
|
|
314
|
-
- Operation involves multiple entities
|
|
315
|
-
- Behavior doesn't belong to one entity
|
|
316
|
-
- Operation is stateless
|
|
317
|
-
|
|
318
|
-
```python
|
|
319
|
-
class TransferService:
|
|
320
|
-
"""Domain service - operates on multiple aggregates"""
|
|
321
|
-
|
|
322
|
-
def transfer(self, from_account: Account, to_account: Account, amount: Money) -> None:
|
|
323
|
-
"""Business logic spanning two aggregates"""
|
|
324
|
-
if from_account.balance < amount.amount:
|
|
325
|
-
raise InsufficientFundsError()
|
|
326
|
-
|
|
327
|
-
# Use entity methods
|
|
328
|
-
from_account.withdraw(amount.amount)
|
|
329
|
-
to_account.deposit(amount.amount)
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
**Note:** Domain services are still pure business logic, not application services (which coordinate IO).
|
|
333
|
-
|
|
334
|
-
### Application Services
|
|
335
|
-
|
|
336
|
-
Coordinate use cases by orchestrating domain objects and repositories.
|
|
337
|
-
|
|
338
|
-
```python
|
|
339
|
-
class OrderApplicationService:
|
|
340
|
-
"""Application service - coordinates use case"""
|
|
341
|
-
|
|
342
|
-
def __init__(
|
|
343
|
-
self,
|
|
344
|
-
order_repo: OrderRepository,
|
|
345
|
-
product_repo: ProductRepository,
|
|
346
|
-
customer_repo: CustomerRepository
|
|
347
|
-
):
|
|
348
|
-
self._order_repo = order_repo
|
|
349
|
-
self._product_repo = product_repo
|
|
350
|
-
self._customer_repo = customer_repo
|
|
351
|
-
|
|
352
|
-
def create_order(self, customer_id: UUID, items: list[OrderItemRequest]) -> UUID:
|
|
353
|
-
"""Use case: create new order"""
|
|
354
|
-
# Validate customer exists
|
|
355
|
-
customer = self._customer_repo.get(customer_id)
|
|
356
|
-
if not customer:
|
|
357
|
-
raise ValueError("Customer not found")
|
|
358
|
-
|
|
359
|
-
# Create order aggregate
|
|
360
|
-
order = Order(id=uuid4(), customer_id=customer_id)
|
|
361
|
-
|
|
362
|
-
# Add items
|
|
363
|
-
for item in items:
|
|
364
|
-
product = self._product_repo.get(item.product_id)
|
|
365
|
-
if not product:
|
|
366
|
-
raise ValueError(f"Product {item.product_id} not found")
|
|
367
|
-
|
|
368
|
-
order.add_line(
|
|
369
|
-
product_id=product.id,
|
|
370
|
-
quantity=item.quantity,
|
|
371
|
-
unit_price=product.price
|
|
372
|
-
)
|
|
373
|
-
|
|
374
|
-
# Save aggregate
|
|
375
|
-
self._order_repo.save(order)
|
|
376
|
-
|
|
377
|
-
return order.id
|
|
378
|
-
|
|
379
|
-
def submit_order(self, order_id: UUID) -> None:
|
|
380
|
-
"""Use case: submit order"""
|
|
381
|
-
order = self._order_repo.get(order_id)
|
|
382
|
-
if not order:
|
|
383
|
-
raise ValueError("Order not found")
|
|
384
|
-
|
|
385
|
-
# Domain logic
|
|
386
|
-
order.submit()
|
|
387
|
-
|
|
388
|
-
# Persist
|
|
389
|
-
self._order_repo.save(order)
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
## Bounded Contexts
|
|
393
|
-
|
|
394
|
-
Separate models for different parts of the domain.
|
|
395
|
-
|
|
396
|
-
```python
|
|
397
|
-
# ===== Catalog Context =====
|
|
398
|
-
@dataclass
|
|
399
|
-
class Product:
|
|
400
|
-
"""Product in catalog context - focused on display"""
|
|
401
|
-
id: UUID
|
|
402
|
-
name: str
|
|
403
|
-
description: str
|
|
404
|
-
price: Money
|
|
405
|
-
category: str
|
|
406
|
-
|
|
407
|
-
# ===== Inventory Context =====
|
|
408
|
-
@dataclass
|
|
409
|
-
class Product:
|
|
410
|
-
"""Product in inventory context - focused on stock"""
|
|
411
|
-
id: UUID
|
|
412
|
-
sku: str
|
|
413
|
-
quantity_on_hand: int
|
|
414
|
-
reorder_point: int
|
|
415
|
-
warehouse_location: str
|
|
416
|
-
|
|
417
|
-
# ===== Order Context =====
|
|
418
|
-
@dataclass
|
|
419
|
-
class Product:
|
|
420
|
-
"""Product in order context - just reference"""
|
|
421
|
-
id: UUID
|
|
422
|
-
name: str
|
|
423
|
-
price: Money
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
**Context Mapping:**
|
|
427
|
-
- **Shared Kernel**: Common types shared across contexts
|
|
428
|
-
- **Customer/Supplier**: Downstream depends on upstream
|
|
429
|
-
- **Anti-Corruption Layer**: Translate between contexts
|
|
430
|
-
|
|
431
|
-
```python
|
|
432
|
-
class CatalogToOrderAdapter:
|
|
433
|
-
"""Anti-corruption layer - translates between contexts"""
|
|
434
|
-
|
|
435
|
-
def __init__(self, catalog_service: CatalogService):
|
|
436
|
-
self._catalog = catalog_service
|
|
437
|
-
|
|
438
|
-
def get_product_for_order(self, product_id: UUID) -> OrderProduct:
|
|
439
|
-
"""Translate catalog product to order product"""
|
|
440
|
-
catalog_product = self._catalog.get_product(product_id)
|
|
441
|
-
return OrderProduct(
|
|
442
|
-
id=catalog_product.id,
|
|
443
|
-
name=catalog_product.name,
|
|
444
|
-
price=catalog_product.price
|
|
445
|
-
)
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
## Specifications Pattern
|
|
449
|
-
|
|
450
|
-
Encapsulate business rules for selection and validation.
|
|
451
|
-
|
|
452
|
-
```python
|
|
453
|
-
from abc import ABC, abstractmethod
|
|
454
|
-
|
|
455
|
-
class Specification(ABC):
|
|
456
|
-
"""Base specification"""
|
|
457
|
-
|
|
458
|
-
@abstractmethod
|
|
459
|
-
def is_satisfied_by(self, candidate: Order) -> bool:
|
|
460
|
-
pass
|
|
461
|
-
|
|
462
|
-
def and_(self, other: "Specification") -> "Specification":
|
|
463
|
-
return AndSpecification(self, other)
|
|
464
|
-
|
|
465
|
-
def or_(self, other: "Specification") -> "Specification":
|
|
466
|
-
return OrSpecification(self, other)
|
|
467
|
-
|
|
468
|
-
class HighValueOrderSpecification(Specification):
|
|
469
|
-
"""Business rule: high value orders"""
|
|
470
|
-
|
|
471
|
-
def __init__(self, threshold: Money):
|
|
472
|
-
self._threshold = threshold
|
|
473
|
-
|
|
474
|
-
def is_satisfied_by(self, candidate: Order) -> bool:
|
|
475
|
-
return candidate.total().amount >= self._threshold.amount
|
|
476
|
-
|
|
477
|
-
class ShippedOrderSpecification(Specification):
|
|
478
|
-
"""Business rule: shipped orders"""
|
|
479
|
-
|
|
480
|
-
def is_satisfied_by(self, candidate: Order) -> bool:
|
|
481
|
-
return candidate.status == "shipped"
|
|
482
|
-
|
|
483
|
-
# Usage
|
|
484
|
-
high_value = HighValueOrderSpecification(Money(Decimal("1000"), "USD"))
|
|
485
|
-
shipped = ShippedOrderSpecification()
|
|
486
|
-
|
|
487
|
-
# Combine specifications
|
|
488
|
-
high_value_shipped = high_value.and_(shipped)
|
|
489
|
-
|
|
490
|
-
if high_value_shipped.is_satisfied_by(order):
|
|
491
|
-
# Apply special handling
|
|
492
|
-
pass
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
## Domain Events
|
|
496
|
-
|
|
497
|
-
Capture significant occurrences in the domain.
|
|
498
|
-
|
|
499
|
-
```python
|
|
500
|
-
from dataclasses import dataclass
|
|
501
|
-
from datetime import datetime
|
|
502
|
-
from uuid import UUID
|
|
503
|
-
|
|
504
|
-
@dataclass
|
|
505
|
-
class DomainEvent:
|
|
506
|
-
"""Base class for domain events"""
|
|
507
|
-
occurred_at: datetime
|
|
508
|
-
|
|
509
|
-
@dataclass
|
|
510
|
-
class OrderSubmitted(DomainEvent):
|
|
511
|
-
"""Event: order was submitted"""
|
|
512
|
-
order_id: UUID
|
|
513
|
-
customer_id: UUID
|
|
514
|
-
total: Money
|
|
515
|
-
|
|
516
|
-
@dataclass
|
|
517
|
-
class Order:
|
|
518
|
-
id: UUID
|
|
519
|
-
customer_id: UUID
|
|
520
|
-
lines: list[OrderLine]
|
|
521
|
-
status: str
|
|
522
|
-
events: list[DomainEvent] = field(default_factory=list)
|
|
523
|
-
|
|
524
|
-
def submit(self) -> None:
|
|
525
|
-
"""Raise domain event when state changes"""
|
|
526
|
-
if self.status != "draft":
|
|
527
|
-
raise ValueError("Only draft orders can be submitted")
|
|
528
|
-
if not self.lines:
|
|
529
|
-
raise ValueError("Cannot submit empty order")
|
|
530
|
-
|
|
531
|
-
self.status = "submitted"
|
|
532
|
-
|
|
533
|
-
# Raise event
|
|
534
|
-
self.events.append(OrderSubmitted(
|
|
535
|
-
occurred_at=datetime.now(),
|
|
536
|
-
order_id=self.id,
|
|
537
|
-
customer_id=self.customer_id,
|
|
538
|
-
total=self.total()
|
|
539
|
-
))
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
## Best Practices
|
|
543
|
-
|
|
544
|
-
1. **Start with the domain**: Model business concepts first, infrastructure later
|
|
545
|
-
2. **Ubiquitous language**: Use business terminology in code
|
|
546
|
-
3. **Small aggregates**: Minimize entities per aggregate
|
|
547
|
-
4. **Reference by ID**: Aggregates reference other aggregates by ID, not object reference
|
|
548
|
-
5. **One aggregate per transaction**: Don't update multiple aggregates in single transaction
|
|
549
|
-
6. **Eventual consistency**: Use domain events for cross-aggregate updates
|
|
550
|
-
7. **Entity behavior**: Put business logic in entities, not services
|
|
551
|
-
8. **Immutable value objects**: Always use `frozen=True` for value objects
|
|
552
|
-
|
|
553
|
-
## Anti-Patterns
|
|
554
|
-
|
|
555
|
-
❌ **Anemic Domain Model**: Entities with only getters/setters, all logic in services
|
|
556
|
-
❌ **God Aggregate**: Aggregate with too many entities
|
|
557
|
-
❌ **Cross-Aggregate Transaction**: Updating multiple aggregates in one transaction
|
|
558
|
-
❌ **Leaky Repository**: Repository exposing database details
|
|
559
|
-
❌ **Service Soup**: Too much logic in services, not enough in entities
|
|
560
|
-
|
|
561
|
-
## Summary
|
|
562
|
-
|
|
563
|
-
- **Entities**: Objects with identity and lifecycle
|
|
564
|
-
- **Value Objects**: Immutable objects defined by attributes
|
|
565
|
-
- **Aggregates**: Transaction boundaries with aggregate root
|
|
566
|
-
- **Repositories**: Collection-like interface for aggregates
|
|
567
|
-
- **Domain Services**: Business logic spanning multiple entities
|
|
568
|
-
- **Application Services**: Use case orchestration with IO
|
|
569
|
-
- **Bounded Contexts**: Separate models for different subdomains
|