@booklib/skills 1.2.0 → 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.
Files changed (100) hide show
  1. package/CONTRIBUTING.md +122 -0
  2. package/README.md +20 -2
  3. package/ROADMAP.md +36 -0
  4. package/animation-at-work/evals/evals.json +44 -0
  5. package/animation-at-work/examples/after.md +64 -0
  6. package/animation-at-work/examples/before.md +35 -0
  7. package/animation-at-work/scripts/audit_animations.py +295 -0
  8. package/bin/skills.js +552 -42
  9. package/clean-code-reviewer/SKILL.md +109 -1
  10. package/clean-code-reviewer/evals/evals.json +121 -3
  11. package/clean-code-reviewer/examples/after.md +48 -0
  12. package/clean-code-reviewer/examples/before.md +33 -0
  13. package/clean-code-reviewer/references/api_reference.md +158 -0
  14. package/clean-code-reviewer/references/practices-catalog.md +282 -0
  15. package/clean-code-reviewer/references/review-checklist.md +254 -0
  16. package/clean-code-reviewer/scripts/pre-review.py +206 -0
  17. package/data-intensive-patterns/evals/evals.json +43 -0
  18. package/data-intensive-patterns/examples/after.md +61 -0
  19. package/data-intensive-patterns/examples/before.md +38 -0
  20. package/data-intensive-patterns/scripts/adr.py +213 -0
  21. package/data-pipelines/evals/evals.json +45 -0
  22. package/data-pipelines/examples/after.md +97 -0
  23. package/data-pipelines/examples/before.md +37 -0
  24. package/data-pipelines/scripts/new_pipeline.py +444 -0
  25. package/design-patterns/evals/evals.json +46 -0
  26. package/design-patterns/examples/after.md +52 -0
  27. package/design-patterns/examples/before.md +29 -0
  28. package/design-patterns/scripts/scaffold.py +807 -0
  29. package/domain-driven-design/SKILL.md +120 -0
  30. package/domain-driven-design/evals/evals.json +48 -0
  31. package/domain-driven-design/examples/after.md +80 -0
  32. package/domain-driven-design/examples/before.md +43 -0
  33. package/domain-driven-design/scripts/scaffold.py +421 -0
  34. package/effective-java/evals/evals.json +46 -0
  35. package/effective-java/examples/after.md +83 -0
  36. package/effective-java/examples/before.md +37 -0
  37. package/effective-java/scripts/checkstyle_setup.py +211 -0
  38. package/effective-kotlin/evals/evals.json +45 -0
  39. package/effective-kotlin/examples/after.md +36 -0
  40. package/effective-kotlin/examples/before.md +38 -0
  41. package/effective-python/evals/evals.json +44 -0
  42. package/effective-python/examples/after.md +56 -0
  43. package/effective-python/examples/before.md +40 -0
  44. package/effective-python/references/api_reference.md +218 -0
  45. package/effective-python/references/practices-catalog.md +483 -0
  46. package/effective-python/references/review-checklist.md +190 -0
  47. package/effective-python/scripts/lint.py +173 -0
  48. package/kotlin-in-action/evals/evals.json +43 -0
  49. package/kotlin-in-action/examples/after.md +53 -0
  50. package/kotlin-in-action/examples/before.md +39 -0
  51. package/kotlin-in-action/scripts/setup_detekt.py +224 -0
  52. package/lean-startup/evals/evals.json +43 -0
  53. package/lean-startup/examples/after.md +80 -0
  54. package/lean-startup/examples/before.md +34 -0
  55. package/lean-startup/scripts/new_experiment.py +286 -0
  56. package/microservices-patterns/SKILL.md +140 -0
  57. package/microservices-patterns/evals/evals.json +45 -0
  58. package/microservices-patterns/examples/after.md +69 -0
  59. package/microservices-patterns/examples/before.md +40 -0
  60. package/microservices-patterns/scripts/new_service.py +583 -0
  61. package/package.json +2 -8
  62. package/refactoring-ui/evals/evals.json +45 -0
  63. package/refactoring-ui/examples/after.md +85 -0
  64. package/refactoring-ui/examples/before.md +58 -0
  65. package/refactoring-ui/scripts/audit_css.py +250 -0
  66. package/skill-router/SKILL.md +142 -0
  67. package/skill-router/evals/evals.json +38 -0
  68. package/skill-router/examples/after.md +63 -0
  69. package/skill-router/examples/before.md +39 -0
  70. package/skill-router/references/api_reference.md +24 -0
  71. package/skill-router/references/routing-heuristics.md +89 -0
  72. package/skill-router/references/skill-catalog.md +156 -0
  73. package/skill-router/scripts/route.py +266 -0
  74. package/storytelling-with-data/evals/evals.json +47 -0
  75. package/storytelling-with-data/examples/after.md +50 -0
  76. package/storytelling-with-data/examples/before.md +33 -0
  77. package/storytelling-with-data/scripts/chart_review.py +301 -0
  78. package/system-design-interview/evals/evals.json +45 -0
  79. package/system-design-interview/examples/after.md +94 -0
  80. package/system-design-interview/examples/before.md +27 -0
  81. package/system-design-interview/scripts/new_design.py +421 -0
  82. package/using-asyncio-python/evals/evals.json +43 -0
  83. package/using-asyncio-python/examples/after.md +68 -0
  84. package/using-asyncio-python/examples/before.md +39 -0
  85. package/using-asyncio-python/scripts/check_blocking.py +270 -0
  86. package/web-scraping-python/evals/evals.json +46 -0
  87. package/web-scraping-python/examples/after.md +109 -0
  88. package/web-scraping-python/examples/before.md +40 -0
  89. package/web-scraping-python/scripts/new_scraper.py +231 -0
  90. /package/{effective-python-skill → effective-python}/SKILL.md +0 -0
  91. /package/{effective-python-skill → effective-python}/ref-01-pythonic-thinking.md +0 -0
  92. /package/{effective-python-skill → effective-python}/ref-02-lists-and-dicts.md +0 -0
  93. /package/{effective-python-skill → effective-python}/ref-03-functions.md +0 -0
  94. /package/{effective-python-skill → effective-python}/ref-04-comprehensions-generators.md +0 -0
  95. /package/{effective-python-skill → effective-python}/ref-05-classes-interfaces.md +0 -0
  96. /package/{effective-python-skill → effective-python}/ref-06-metaclasses-attributes.md +0 -0
  97. /package/{effective-python-skill → effective-python}/ref-07-concurrency.md +0 -0
  98. /package/{effective-python-skill → effective-python}/ref-08-robustness-performance.md +0 -0
  99. /package/{effective-python-skill → effective-python}/ref-09-testing-debugging.md +0 -0
  100. /package/{effective-python-skill → effective-python}/ref-10-collaboration.md +0 -0
@@ -219,3 +219,123 @@ Priority-ordered list of improvements, from most critical to nice-to-have.
219
219
  but the patterns are framework-agnostic.
220
220
  - For deeper pattern details, read `references/patterns-catalog.md` before generating code.
221
221
  - For review checklists, read `references/review-checklist.md` before reviewing code.
222
+
223
+ ---
224
+
225
+ ## Mode 3: Domain Migration Planning
226
+
227
+ **Trigger phrases:** "migrate to DDD", "enrich my domain model", "extract value objects from", "refactor toward DDD", "strangler fig for domain"
228
+
229
+ You are helping a developer incrementally migrate an existing codebase toward Domain-Driven Design — without a full rewrite. The goal is a **phased migration plan** that progressively enriches the domain model, reduces Primitive Obsession, and establishes proper Aggregate boundaries.
230
+
231
+ ### Step 1 — Assess Current State
232
+
233
+ Classify the codebase as one of:
234
+ - **Transaction Script** — Procedural service methods manipulating passive data objects. All logic in services, domain objects are mere structs.
235
+ - **Anemic Domain Model** — Classes look like Entities but have only getters/setters. Business logic lives in services.
236
+ - **Partial DDD** — Some patterns applied (e.g., Value Objects exist) but boundaries are fuzzy or Aggregates are anemic.
237
+
238
+ Identify the worst anti-patterns present (Primitive Obsession, missing invariant enforcement, broken Bounded Contexts, leaking infrastructure).
239
+
240
+ ### Step 2 — Phase 1: Ubiquitous Language (Zero-Risk)
241
+
242
+ **Goal:** Rename classes and methods to domain terms. No structural change.
243
+ **Risk:** Near zero — rename-only refactoring.
244
+
245
+ Actions:
246
+ - Rename technical names to domain language (e.g., `UserData` → `Customer`, `ItemManager` → `InventoryService`)
247
+ - Build a Ubiquitous Language glossary mapping old names → new names
248
+ - Ensure method names reflect domain operations (e.g., `updateStatus(2)` → `approve()`)
249
+
250
+ **Definition of Done:** A domain expert can read class and method names without a translator.
251
+
252
+ ### Step 3 — Phase 2: Value Objects (Low-Risk)
253
+
254
+ **Goal:** Extract Primitive Obsession into immutable Value Objects.
255
+ **Risk:** Low — additive change; old primitives gradually replaced.
256
+
257
+ Actions:
258
+ - Identify primitives used as domain concepts: orderId (String), price (double), email (String)
259
+ - Create immutable Value Objects with validation in constructor: `OrderId`, `Money`, `Email`
260
+ - Replace primitive usages one class at a time
261
+ - Add equality semantics (attribute-based equality, not identity)
262
+
263
+ Before: `String email = "user@example.com";`
264
+ After: `Email email = Email.of("user@example.com"); // validates format`
265
+
266
+ **Definition of Done:** No primitive types represent domain concepts in Entity constructors or method signatures.
267
+
268
+ ### Step 4 — Phase 3: Aggregate Boundaries (Medium-Risk)
269
+
270
+ **Goal:** Define Aggregate roots and enforce invariants inside them.
271
+ **Risk:** Medium — changes cascade to callers and repositories.
272
+
273
+ Actions:
274
+ - Identify clusters of objects that change together (Aggregate candidates)
275
+ - Designate Aggregate roots; route all external access through them
276
+ - Remove external setters; enforce invariants via domain methods
277
+ - Reference other Aggregates by ID only (no direct object references across boundaries)
278
+ - Wrap related creation logic in Factory methods
279
+
280
+ **Definition of Done:** No code outside an Aggregate can violate its invariants. Cross-Aggregate references are by ID only.
281
+
282
+ ### Step 5 — Phase 4: Repositories & Domain Services (Medium-Risk)
283
+
284
+ **Goal:** Add Repository interfaces per Aggregate root; extract Domain Services.
285
+ **Risk:** Medium — requires infrastructure layer changes.
286
+
287
+ Actions:
288
+ - Add a Repository interface (domain layer) for each Aggregate root
289
+ - Move persistence implementation to infrastructure; domain layer knows nothing about databases
290
+ - Extract operations that don't belong to any Entity into stateless Domain Services
291
+ - Name Domain Services in Ubiquitous Language
292
+
293
+ **Definition of Done:** Domain layer has zero imports from persistence frameworks. Each Aggregate root has exactly one Repository.
294
+
295
+ ### Step 6 — Phase 5: Strategic Design (High-Risk, Optional)
296
+
297
+ **Goal:** Identify Bounded Contexts; protect domain model from external systems.
298
+ **Risk:** High — may require restructuring module/package boundaries.
299
+
300
+ Actions:
301
+ - Map Bounded Contexts (where does one domain model end and another begin?)
302
+ - Build Anticorruption Layers for external integrations (legacy systems, third-party APIs)
303
+ - Identify Shared Kernels vs. Customer/Supplier relationships between contexts
304
+ - Apply Strangler Fig if migrating from a monolith: route new domain through new model, keep legacy running
305
+
306
+ **Definition of Done:** Each Bounded Context has a clear boundary. External models don't corrupt the core domain.
307
+
308
+ ### Migration Output Format
309
+
310
+ ```
311
+ ## DDD Migration Plan: [System/Module Name]
312
+
313
+ ### Current State Assessment
314
+ **Classification:** Anemic Domain Model
315
+ **Key anti-patterns:** Primitive Obsession (orderId as String), external setters on Order, all logic in OrderService
316
+
317
+ ### Phase 1 — Ubiquitous Language (start now)
318
+ - [ ] Rename `OrderData` → `Order`
319
+ - [ ] Rename `processOrder()` → `placeOrder()`
320
+ **Glossary:**
321
+ | Old Name | New Name | Reason |
322
+ |----------|----------|--------|
323
+ | OrderData | Order | Domain entity, not a data bag |
324
+
325
+ ### Phase 2 — Value Objects (next sprint)
326
+ - [ ] Extract `OrderId` from `String orderId`
327
+ - [ ] Extract `Money` from `double price`
328
+ **Before:** `double price = 99.99;`
329
+ **After:** `Money price = Money.of(new BigDecimal("99.99"), Currency.USD);`
330
+
331
+ ### Phase 3 — Aggregate Boundaries (following sprint)
332
+ - [ ] Make Order the Aggregate root; remove direct LineItem mutation from OrderController
333
+ - [ ] Add `Order.addLineItem(LineItem)` enforcing max-items invariant
334
+
335
+ ### Phase 4 — Repositories & Services (planned)
336
+ - [ ] Add `OrderRepository` interface in domain layer
337
+ - [ ] Extract `PricingService` from `OrderService.calculateTotal()`
338
+
339
+ ### Phase 5 — Strategic Design (future)
340
+ - [ ] Identify Billing Context boundary; add ACL to translate Payment gateway model
341
+ ```
@@ -0,0 +1,48 @@
1
+ {
2
+ "evals": [
3
+ {
4
+ "id": "eval-01-anemic-domain-model",
5
+ "prompt": "Review this code for a loan application system:\n\n```java\n// Domain object — just data\npublic class LoanApplication {\n private String applicantId;\n private BigDecimal requestedAmount;\n private int termMonths;\n private String status; // PENDING, APPROVED, REJECTED, DISBURSED\n private BigDecimal approvedAmount;\n private String rejectionReason;\n private LocalDate applicationDate;\n private String reviewerId;\n \n // getters and setters for all fields...\n}\n\n// All logic lives here\npublic class LoanApplicationService {\n private LoanApplicationRepository repository;\n private CreditScoreService creditScoreService;\n private RiskEngine riskEngine;\n \n public void approveLoan(String applicationId, String reviewerId, BigDecimal approvedAmount) {\n LoanApplication application = repository.findById(applicationId);\n if (!application.getStatus().equals(\"PENDING\")) {\n throw new IllegalStateException(\"Can only approve PENDING applications\");\n }\n if (approvedAmount.compareTo(application.getRequestedAmount()) > 0) {\n throw new IllegalArgumentException(\"Approved amount cannot exceed requested amount\");\n }\n application.setStatus(\"APPROVED\");\n application.setApprovedAmount(approvedAmount);\n application.setReviewerId(reviewerId);\n repository.save(application);\n }\n \n public void rejectLoan(String applicationId, String reviewerId, String reason) {\n LoanApplication application = repository.findById(applicationId);\n if (!application.getStatus().equals(\"PENDING\")) {\n throw new IllegalStateException(\"Can only reject PENDING applications\");\n }\n application.setStatus(\"REJECTED\");\n application.setRejectionReason(reason);\n application.setReviewerId(reviewerId);\n repository.save(application);\n }\n \n public boolean isEligibleForFastTrack(String applicationId) {\n LoanApplication application = repository.findById(applicationId);\n int creditScore = creditScoreService.getScore(application.getApplicantId());\n return creditScore > 750 && application.getRequestedAmount().compareTo(new BigDecimal(\"50000\")) <= 0;\n }\n}\n```",
6
+ "expectations": [
7
+ "Identifies this as an Anemic Domain Model — LoanApplication is a data bag with no behavior, all logic pushed into the service layer",
8
+ "Flags that approve() and reject() are domain operations that belong on the LoanApplication aggregate, not in the service",
9
+ "Notes that the invariant 'can only approve PENDING applications' is a business rule that LoanApplication should enforce internally",
10
+ "Flags Primitive Obsession: status as String instead of a LoanStatus enum or Value Object; applicantId and reviewerId as raw Strings instead of typed Value Objects (ApplicantId, ReviewerId)",
11
+ "Notes that requestedAmount and approvedAmount should be a Money Value Object (amount + currency), not a bare BigDecimal",
12
+ "Points out that the service directly sets fields via setters (setStatus, setApprovedAmount) — bypasses any invariant enforcement and breaks encapsulation",
13
+ "Recommends moving approve(ReviewerId reviewerId, Money approvedAmount) and reject(ReviewerId reviewerId, String reason) methods onto LoanApplication itself",
14
+ "Suggests that the service layer should be thin: load aggregate → call domain method → save",
15
+ "References the anti-pattern: 'Transaction Script masquerading as DDD — procedural service methods that manipulate passive data objects'"
16
+ ]
17
+ },
18
+ {
19
+ "id": "eval-02-missing-value-object",
20
+ "prompt": "Review this code for a shipping system:\n\n```java\npublic class Shipment {\n private String id;\n private String recipientName;\n private String streetAddress;\n private String city;\n private String stateCode;\n private String postalCode;\n private String countryCode;\n private String senderName;\n private String senderStreet;\n private String senderCity;\n private String senderStateCode;\n private String senderPostalCode;\n private String senderCountryCode;\n private double weightKg;\n private double lengthCm;\n private double widthCm;\n private double heightCm;\n private String trackingNumber;\n private String status;\n \n public double calculateVolumetricWeight() {\n return (lengthCm * widthCm * heightCm) / 5000.0;\n }\n \n public double getBillableWeight() {\n double volumetric = calculateVolumetricWeight();\n return Math.max(weightKg, volumetric);\n }\n \n public boolean isInternational() {\n return !senderCountryCode.equals(countryCode);\n }\n \n public String formatRecipientAddress() {\n return recipientName + \"\\n\" + streetAddress + \"\\n\" +\n city + \", \" + stateCode + \" \" + postalCode + \"\\n\" + countryCode;\n }\n \n public String formatSenderAddress() {\n return senderName + \"\\n\" + senderStreet + \"\\n\" +\n senderCity + \", \" + senderStateCode + \" \" + senderPostalCode + \"\\n\" + senderCountryCode;\n }\n}\n```",
21
+ "expectations": [
22
+ "Identifies the missing Value Objects: Address (street, city, state, postal code, country) is a natural concept that is duplicated for sender and recipient",
23
+ "Identifies a second Value Object: Dimensions (length, width, height) with volumetric weight behavior",
24
+ "Identifies a third Value Object candidate: Weight (value + unit — kg is baked into the field name but not the type)",
25
+ "Notes that the Shipment Entity is bloated with 19 fields — a God Entity collecting too many responsibilities",
26
+ "Points out that formatRecipientAddress() and formatSenderAddress() are the same logic duplicated with different field names — a symptom of not having an Address Value Object",
27
+ "Notes that calculateVolumetricWeight() belongs on a Dimensions Value Object, not on Shipment",
28
+ "Flags Primitive Obsession: all fields are raw strings/doubles instead of typed domain concepts",
29
+ "Recommends concrete refactoring: extract Address, Dimensions, Weight Value Objects; Shipment holds Address recipient, Address sender, Dimensions, Weight",
30
+ "Notes Value Objects should be immutable with attribute-based equality"
31
+ ]
32
+ },
33
+ {
34
+ "id": "eval-03-well-designed-aggregate",
35
+ "prompt": "Review this domain model for a bank account system:\n\n```java\npublic class AccountId {\n private final String value;\n \n public AccountId(String value) {\n if (value == null || value.isBlank()) throw new IllegalArgumentException(\"AccountId cannot be blank\");\n this.value = value;\n }\n \n public String getValue() { return value; }\n \n @Override public boolean equals(Object o) {\n if (this == o) return true;\n if (!(o instanceof AccountId)) return false;\n return value.equals(((AccountId) o).value);\n }\n @Override public int hashCode() { return value.hashCode(); }\n}\n\npublic class Money {\n private final BigDecimal amount;\n private final Currency currency;\n \n public Money(BigDecimal amount, Currency currency) {\n if (amount == null || currency == null) throw new IllegalArgumentException();\n this.amount = amount;\n this.currency = currency;\n }\n \n public Money add(Money other) {\n if (!this.currency.equals(other.currency)) throw new IllegalArgumentException(\"Currency mismatch\");\n return new Money(this.amount.add(other.amount), this.currency);\n }\n \n public Money subtract(Money other) {\n if (!this.currency.equals(other.currency)) throw new IllegalArgumentException(\"Currency mismatch\");\n return new Money(this.amount.subtract(other.amount), this.currency);\n }\n \n public boolean isNegative() { return amount.compareTo(BigDecimal.ZERO) < 0; }\n \n // equals, hashCode based on amount and currency\n}\n\npublic class BankAccount {\n private final AccountId id;\n private Money balance;\n private final List<Transaction> ledger;\n private AccountStatus status;\n \n private BankAccount(AccountId id, Money initialBalance) {\n this.id = id;\n this.balance = initialBalance;\n this.ledger = new ArrayList<>();\n this.status = AccountStatus.ACTIVE;\n }\n \n public static BankAccount open(AccountId id, Money initialDeposit) {\n if (initialDeposit.isNegative()) throw new IllegalArgumentException(\"Initial deposit must be positive\");\n return new BankAccount(id, initialDeposit);\n }\n \n public void deposit(Money amount) {\n if (status != AccountStatus.ACTIVE) throw new IllegalStateException(\"Cannot deposit to a \" + status + \" account\");\n if (amount.isNegative()) throw new IllegalArgumentException(\"Deposit amount must be positive\");\n this.balance = this.balance.add(amount);\n ledger.add(Transaction.credit(amount));\n }\n \n public void withdraw(Money amount) {\n if (status != AccountStatus.ACTIVE) throw new IllegalStateException(\"Cannot withdraw from a \" + status + \" account\");\n if (amount.isNegative()) throw new IllegalArgumentException(\"Withdrawal amount must be positive\");\n Money newBalance = this.balance.subtract(amount);\n if (newBalance.isNegative()) throw new DomainException(\"Insufficient funds\");\n this.balance = newBalance;\n ledger.add(Transaction.debit(amount));\n }\n \n public void close() {\n if (!balance.equals(Money.zero(balance.getCurrency()))) {\n throw new DomainException(\"Cannot close account with non-zero balance\");\n }\n this.status = AccountStatus.CLOSED;\n }\n \n public Money getBalance() { return balance; }\n public List<Transaction> getLedger() { return Collections.unmodifiableList(ledger); }\n public AccountId getId() { return id; }\n}\n```",
36
+ "expectations": [
37
+ "Recognizes this as a well-designed Aggregate and says so explicitly",
38
+ "Praises AccountId and Money as properly modeled Value Objects — immutable, attribute-based equality, validated in constructor",
39
+ "Praises the factory method BankAccount.open() for enforcing invariants at creation time",
40
+ "Praises that all business rules (no overdraft, no deposit to closed account, cannot close with balance) are enforced inside the aggregate rather than in a service layer",
41
+ "Praises that getLedger() returns an unmodifiable list — protecting the internal collection from external mutation",
42
+ "Praises the closed constructor — external code cannot create a BankAccount without going through the factory method",
43
+ "Does NOT manufacture fake issues just to have something to say",
44
+ "May offer optional suggestions (domain events for deposit/withdrawal, separate TransactionId Value Object) but clearly frames them as enhancements, not defects"
45
+ ]
46
+ }
47
+ ]
48
+ }
@@ -0,0 +1,80 @@
1
+ # After
2
+
3
+ `Order` becomes a rich aggregate root that owns its invariants and exposes domain-language behavior; `OrderService` becomes a thin application-layer coordinator.
4
+
5
+ ```java
6
+ // Rich aggregate root — enforces all invariants internally
7
+ public class Order {
8
+ private final OrderId id;
9
+ private final List<OrderLine> lines = new ArrayList<>();
10
+ private OrderStatus status;
11
+ private Money total;
12
+
13
+ private Order(OrderId id, CustomerId customerId) {
14
+ this.id = id;
15
+ this.status = OrderStatus.PENDING;
16
+ this.total = Money.ZERO;
17
+ }
18
+
19
+ public static Order place(OrderId id, CustomerId customerId) {
20
+ return new Order(id, customerId);
21
+ }
22
+
23
+ public void addItem(Product product, int quantity) {
24
+ Validate.isTrue(quantity > 0, "Quantity must be positive");
25
+ Validate.isTrue(status == OrderStatus.PENDING, "Cannot modify a non-pending order");
26
+ lines.add(OrderLine.of(product.getId(), product.getPrice(), quantity));
27
+ recalculateTotal();
28
+ }
29
+
30
+ public void confirm() {
31
+ if (lines.isEmpty()) {
32
+ throw new DomainException("Cannot confirm an order with no items");
33
+ }
34
+ this.status = OrderStatus.CONFIRMED;
35
+ }
36
+
37
+ public void cancel() {
38
+ if (!status.isCancellable()) {
39
+ throw new DomainException("Order in status " + status + " cannot be cancelled");
40
+ }
41
+ this.status = OrderStatus.CANCELLED;
42
+ }
43
+
44
+ private void recalculateTotal() {
45
+ this.total = lines.stream()
46
+ .map(OrderLine::lineTotal)
47
+ .reduce(Money.ZERO, Money::add);
48
+ }
49
+
50
+ // Getters return immutable views only
51
+ public List<OrderLine> lines() { return Collections.unmodifiableList(lines); }
52
+ public Money total() { return total; }
53
+ public OrderStatus status() { return status; }
54
+ }
55
+
56
+ // Application service is thin — orchestrates, does not contain business rules
57
+ public class PlaceOrderUseCase {
58
+ private final OrderRepository orderRepository;
59
+ private final ProductRepository productRepository;
60
+
61
+ public OrderId execute(PlaceOrderCommand cmd) {
62
+ Order order = Order.place(OrderId.generate(), cmd.customerId());
63
+ for (var item : cmd.requestedItems()) {
64
+ Product product = productRepository.findById(item.productId())
65
+ .orElseThrow(() -> new ProductNotFoundException(item.productId()));
66
+ order.addItem(product, item.quantity());
67
+ }
68
+ order.confirm();
69
+ orderRepository.save(order);
70
+ return order.getId();
71
+ }
72
+ }
73
+ ```
74
+
75
+ Key improvements:
76
+ - Business rules (`confirm`, `cancel`, `addItem`) live inside `Order` — the anemic model anti-pattern is eliminated
77
+ - `Order.status.isCancellable()` encapsulates the cancellation-eligibility rule in an enum method rather than in a service
78
+ - External code cannot corrupt the aggregate: `lines` is returned as an unmodifiable view; `total` is always recalculated internally
79
+ - `OrderService` is renamed to `PlaceOrderUseCase` per the Ubiquitous Language (application command, not a generic "service")
80
+ - `Money` is a Value Object, preventing `double` arithmetic errors on financial amounts (Primitive Obsession eliminated)
@@ -0,0 +1,43 @@
1
+ # Before
2
+
3
+ An anemic domain model where the `Order` class is a passive data container with no behavior, and all business logic is scattered across `OrderService`.
4
+
5
+ ```java
6
+ // Anemic entity — just a data bag
7
+ public class Order {
8
+ public Long id;
9
+ public List<OrderLine> lines;
10
+ public String status;
11
+ public BigDecimal totalAmount;
12
+ public String customerId;
13
+ }
14
+
15
+ // All logic lives in the service
16
+ public class OrderService {
17
+
18
+ public void addItem(Order order, Product product, int quantity) {
19
+ OrderLine line = new OrderLine();
20
+ line.productId = product.id;
21
+ line.quantity = quantity;
22
+ line.unitPrice = product.price;
23
+ order.lines.add(line);
24
+ order.totalAmount = order.lines.stream()
25
+ .map(l -> l.unitPrice.multiply(BigDecimal.valueOf(l.quantity)))
26
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
27
+ }
28
+
29
+ public void cancel(Order order) {
30
+ if (!order.status.equals("PENDING") && !order.status.equals("CONFIRMED")) {
31
+ throw new IllegalStateException("Cannot cancel");
32
+ }
33
+ order.status = "CANCELLED";
34
+ }
35
+
36
+ public void confirm(Order order) {
37
+ if (order.lines == null || order.lines.isEmpty()) {
38
+ throw new IllegalStateException("Cannot confirm empty order");
39
+ }
40
+ order.status = "CONFIRMED";
41
+ }
42
+ }
43
+ ```