@martinffx/atelier 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.
Files changed (130) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +285 -0
  4. package/agents/architect.md +38 -0
  5. package/agents/oracle.md +40 -0
  6. package/agents/recon.md +39 -0
  7. package/dist/atelier.js +33082 -0
  8. package/package.json +44 -0
  9. package/skills/code-conventional-commit/SKILL.md +134 -0
  10. package/skills/code-conventional-commit/evals/evals.json +23 -0
  11. package/skills/code-conventional-commit/workspace/iteration-1/eval-0/with_skill/outputs/commit-message.txt +4 -0
  12. package/skills/code-conventional-commit/workspace/iteration-1/eval-0/with_skill/outputs/eval_metadata.json +6 -0
  13. package/skills/code-conventional-commit/workspace/iteration-1/eval-1/with_skill/outputs/eval_metadata.json +6 -0
  14. package/skills/code-conventional-commit/workspace/iteration-1/eval-1/with_skill/outputs/validation-result.txt +16 -0
  15. package/skills/code-conventional-commit/workspace/iteration-1/eval-2/with_skill/outputs/commit-message.txt +8 -0
  16. package/skills/code-conventional-commit/workspace/iteration-1/eval-2/with_skill/outputs/eval_metadata.json +6 -0
  17. package/skills/code-conventional-commit/workspace/iteration-1/report.html +1325 -0
  18. package/skills/code-debug/SKILL.md +271 -0
  19. package/skills/code-debug/references/language-tools.md +139 -0
  20. package/skills/code-debug/references/techniques.md +117 -0
  21. package/skills/code-docs/SKILL.md +171 -0
  22. package/skills/code-docs/references/api-docs.md +116 -0
  23. package/skills/code-docs/references/changelog.md +64 -0
  24. package/skills/code-docs/references/readme-structure.md +94 -0
  25. package/skills/code-git-worktrees/SKILL.md +218 -0
  26. package/skills/code-perf/SKILL.md +163 -0
  27. package/skills/code-perf/references/common-bottlenecks.md +124 -0
  28. package/skills/code-perf/references/optimization-patterns.md +167 -0
  29. package/skills/code-perf/references/profiling-tools.md +126 -0
  30. package/skills/code-review/SKILL.md +78 -0
  31. package/skills/code-review/references/output.md +200 -0
  32. package/skills/code-review/references/reviewers.md +719 -0
  33. package/skills/code-review/references/rq.md +357 -0
  34. package/skills/code-review/references/rs.md +30 -0
  35. package/skills/code-security/SKILL.md +183 -0
  36. package/skills/code-security/references/owasp-top-10.md +129 -0
  37. package/skills/code-security/references/security-tools.md +102 -0
  38. package/skills/code-security/references/vulnerability-patterns.md +129 -0
  39. package/skills/code-stacked-commit/SKILL.md +155 -0
  40. package/skills/code-subagents/SKILL.md +184 -0
  41. package/skills/code-subagents/references/code-quality-reviewer-prompt.md +48 -0
  42. package/skills/code-subagents/references/implementor-prompt.md +79 -0
  43. package/skills/code-subagents/references/spec-reviewer-prompt.md +62 -0
  44. package/skills/oracle-architect/SKILL.md +246 -0
  45. package/skills/oracle-architect/references/api-design.md +345 -0
  46. package/skills/oracle-architect/references/data-modeling.md +318 -0
  47. package/skills/oracle-architect/references/ddd-patterns.md +247 -0
  48. package/skills/oracle-challenge/SKILL.md +126 -0
  49. package/skills/oracle-testing/SKILL.md +237 -0
  50. package/skills/oracle-testing/references/anti-patterns.md +618 -0
  51. package/skills/oracle-testing/references/boundaries.md +579 -0
  52. package/skills/oracle-testing/references/core-testing.md +691 -0
  53. package/skills/oracle-testing/references/edge-testing.md +794 -0
  54. package/skills/oracle-testing/references/stub-driven-tdd.md +322 -0
  55. package/skills/oracle-thinkdeep/SKILL.md +164 -0
  56. package/skills/python-architecture/SKILL.md +148 -0
  57. package/skills/python-architecture/references/data-modeling.md +558 -0
  58. package/skills/python-architecture/references/ddd.md +569 -0
  59. package/skills/python-architecture/references/functional-core.md +337 -0
  60. package/skills/python-build-tools/SKILL.md +153 -0
  61. package/skills/python-build-tools/references/basedpyright.md +321 -0
  62. package/skills/python-build-tools/references/ruff.md +225 -0
  63. package/skills/python-build-tools/references/uv.md +257 -0
  64. package/skills/python-fastapi/SKILL.md +184 -0
  65. package/skills/python-fastapi/references/api-design.md +563 -0
  66. package/skills/python-fastapi/references/dependencies.md +479 -0
  67. package/skills/python-fastapi/references/middleware.md +475 -0
  68. package/skills/python-fastapi/references/validation.md +497 -0
  69. package/skills/python-modern-python/SKILL.md +201 -0
  70. package/skills/python-modern-python/references/async.md +76 -0
  71. package/skills/python-modern-python/references/pattern-matching.md +89 -0
  72. package/skills/python-modern-python/references/typing.md +96 -0
  73. package/skills/python-monorepo/SKILL.md +146 -0
  74. package/skills/python-monorepo/references/docker.md +515 -0
  75. package/skills/python-monorepo/references/namespace-packages.md +426 -0
  76. package/skills/python-monorepo/references/workspace-config.md +341 -0
  77. package/skills/python-sqlalchemy/SKILL.md +119 -0
  78. package/skills/python-sqlalchemy/references/async.md +51 -0
  79. package/skills/python-sqlalchemy/references/models.md +67 -0
  80. package/skills/python-sqlalchemy/references/queries.md +57 -0
  81. package/skills/python-temporal/SKILL.md +128 -0
  82. package/skills/python-temporal/references/patterns.md +87 -0
  83. package/skills/python-temporal/references/testing.md +54 -0
  84. package/skills/python-testing/SKILL.md +161 -0
  85. package/skills/python-testing/references/boundaries.md +449 -0
  86. package/skills/python-testing/references/mocking.md +449 -0
  87. package/skills/python-testing/references/pytest.md +524 -0
  88. package/skills/spec-brainstorm/SKILL.md +325 -0
  89. package/skills/spec-finish/SKILL.md +159 -0
  90. package/skills/spec-implement/SKILL.md +296 -0
  91. package/skills/spec-orchestrator/SKILL.md +156 -0
  92. package/skills/spec-plan/SKILL.md +248 -0
  93. package/skills/typescript-api-design/SKILL.md +396 -0
  94. package/skills/typescript-api-design/references/error-responses.md +469 -0
  95. package/skills/typescript-build-tools/SKILL.md +447 -0
  96. package/skills/typescript-build-tools/references/biome.md +436 -0
  97. package/skills/typescript-build-tools/references/bun.md +432 -0
  98. package/skills/typescript-build-tools/references/turborepo.md +426 -0
  99. package/skills/typescript-build-tools/references/vitest.md +518 -0
  100. package/skills/typescript-drizzle-orm/SKILL.md +398 -0
  101. package/skills/typescript-drizzle-orm/references/cloudflare.md +392 -0
  102. package/skills/typescript-drizzle-orm/references/entity-pattern.md +412 -0
  103. package/skills/typescript-drizzle-orm/references/postgresql.md +191 -0
  104. package/skills/typescript-drizzle-orm/references/repository-pattern.md +479 -0
  105. package/skills/typescript-drizzle-orm/references/sqlite.md +291 -0
  106. package/skills/typescript-dynamodb-toolbox/SKILL.md +370 -0
  107. package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +574 -0
  108. package/skills/typescript-dynamodb-toolbox/references/error-handling.md +495 -0
  109. package/skills/typescript-dynamodb-toolbox/references/modeling.md +332 -0
  110. package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +631 -0
  111. package/skills/typescript-dynamodb-toolbox/references/testing.md +718 -0
  112. package/skills/typescript-dynamodb-toolbox/references/transactions.md +423 -0
  113. package/skills/typescript-effect-ts/SKILL.md +341 -0
  114. package/skills/typescript-effect-ts/references/core.md +412 -0
  115. package/skills/typescript-effect-ts/references/error-handling.md +468 -0
  116. package/skills/typescript-effect-ts/references/resources.md +476 -0
  117. package/skills/typescript-effect-ts/references/schema.md +536 -0
  118. package/skills/typescript-effect-ts/references/services.md +461 -0
  119. package/skills/typescript-fastify/SKILL.md +405 -0
  120. package/skills/typescript-fastify/references/plugins.md +329 -0
  121. package/skills/typescript-fastify/references/typeid.md +315 -0
  122. package/skills/typescript-functional-patterns/SKILL.md +402 -0
  123. package/skills/typescript-functional-patterns/references/adts.md +497 -0
  124. package/skills/typescript-functional-patterns/references/branded-types.md +542 -0
  125. package/skills/typescript-functional-patterns/references/migration-guide.md +695 -0
  126. package/skills/typescript-functional-patterns/references/option-result.md +585 -0
  127. package/skills/typescript-testing/SKILL.md +353 -0
  128. package/skills/typescript-testing/references/mocking.md +675 -0
  129. package/skills/typescript-testing/references/msw.md +852 -0
  130. package/skills/typescript-testing/references/snapshot-testing.md +774 -0
@@ -0,0 +1,569 @@
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