dflow-sdd-ddd 0.9.0 → 0.11.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 (57) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.en.md +73 -48
  3. package/README.md +46 -36
  4. package/TEMPLATE-COVERAGE.md +0 -1
  5. package/TEMPLATE-LANGUAGE-GLOSSARY.md +1 -0
  6. package/bin/dflow.js +7 -11
  7. package/docs/evaluating-dflow.en.md +11 -7
  8. package/docs/evaluating-dflow.md +9 -4
  9. package/docs/using-with-claude-code.en.md +40 -23
  10. package/docs/using-with-claude-code.md +34 -23
  11. package/docs/using-with-codex.en.md +125 -42
  12. package/docs/using-with-codex.md +93 -34
  13. package/docs/using-with-github-copilot.en.md +135 -34
  14. package/docs/using-with-github-copilot.md +120 -43
  15. package/docs/why-dflow.en.md +72 -0
  16. package/docs/why-dflow.md +72 -0
  17. package/lib/init.js +867 -214
  18. package/package.json +2 -2
  19. package/templates/brownfield/references/drift-verification.md +41 -10
  20. package/templates/brownfield/references/finish-feature-flow.md +3 -2
  21. package/templates/brownfield/references/git-integration.md +0 -1
  22. package/templates/brownfield/references/init-project-flow.md +31 -17
  23. package/templates/brownfield/references/modify-existing-flow.md +44 -38
  24. package/templates/brownfield/references/new-feature-flow.md +41 -11
  25. package/templates/brownfield/references/new-phase-flow.md +9 -2
  26. package/templates/brownfield/references/pr-review-checklist.md +7 -1
  27. package/templates/brownfield/scaffolding/AI-AGENT-GUIDE.md +258 -29
  28. package/templates/brownfield/scaffolding/CLAUDE-md-snippet.md +5 -8
  29. package/templates/brownfield/scaffolding/_conventions.md +10 -9
  30. package/templates/brownfield/templates/_index.md +1 -1
  31. package/templates/brownfield/templates/context-map.md +12 -4
  32. package/templates/brownfield/templates/lightweight-spec.md +1 -1
  33. package/templates/brownfield/templates/phase-spec.md +1 -1
  34. package/templates/common/references/ddd-modeling-guide.md +643 -0
  35. package/templates/common/skill/SKILL.md +9 -6
  36. package/templates/greenfield/references/drift-verification.md +60 -15
  37. package/templates/greenfield/references/finish-feature-flow.md +3 -2
  38. package/templates/greenfield/references/git-integration.md +0 -1
  39. package/templates/greenfield/references/init-project-flow.md +31 -17
  40. package/templates/greenfield/references/modify-existing-flow.md +5 -7
  41. package/templates/greenfield/references/new-feature-flow.md +49 -19
  42. package/templates/greenfield/references/new-phase-flow.md +5 -2
  43. package/templates/greenfield/references/pr-review-checklist.md +9 -1
  44. package/templates/greenfield/scaffolding/AI-AGENT-GUIDE.md +221 -29
  45. package/templates/greenfield/scaffolding/CLAUDE-md-snippet.md +10 -15
  46. package/templates/greenfield/scaffolding/Git-principles-trunk.md +1 -1
  47. package/templates/greenfield/scaffolding/_conventions.md +9 -8
  48. package/templates/greenfield/templates/_index.md +1 -1
  49. package/templates/greenfield/templates/aggregate-design.md +6 -0
  50. package/templates/greenfield/templates/context-map.md +13 -4
  51. package/templates/greenfield/templates/events.md +4 -1
  52. package/templates/greenfield/templates/lightweight-spec.md +1 -1
  53. package/templates/greenfield/templates/phase-spec.md +1 -1
  54. package/docs/migrating-to-dflow-v1.md +0 -230
  55. package/templates/brownfield/templates/CLAUDE.md +0 -165
  56. package/templates/greenfield/references/ddd-modeling-guide.md +0 -351
  57. package/templates/greenfield/templates/CLAUDE.md +0 -172
@@ -0,0 +1,643 @@
1
+ # DDD Modeling Guide
2
+
3
+ When a developer asks "How should I model X?" or is designing domain structures,
4
+ use this guide to walk them through DDD tactical patterns.
5
+
6
+ > **Edition note — where to record decisions.** The artifact names below are the
7
+ > Greenfield spec surfaces (`aggregate-design.md` worksheet, `events.md`, ADRs
8
+ > under `architecture/decisions/`). Brownfield projects do not seed those, so
9
+ > record the same decision on the surface you have:
10
+ >
11
+ > - **Aggregate / invariant design** → the `models.md` Aggregate-Root Entity row
12
+ > + `rules.md` (Brownfield has no separate `aggregate-design.md` worksheet —
13
+ > do not invent one).
14
+ > - **Event contracts / failure-path notes** → `behavior.md` (as a `BR-*`
15
+ > scenario) and/or `migration/tech-debt.md` (Brownfield has no `events.md`).
16
+ > - **Event Sourcing / architecture decisions** → an existing ADR or migration
17
+ > plan if the project already has one; otherwise `migration/tech-debt.md`
18
+ > § Follow-up Notes and/or the target-architecture strategy section of
19
+ > `_overview.md`.
20
+ >
21
+ > The tactical patterns themselves are identical across editions; only the
22
+ > recording surface differs.
23
+
24
+ ## The Modeling Conversation
25
+
26
+ Start with the business, not the code:
27
+
28
+ ```
29
+ "Let's forget about databases and classes for a moment.
30
+ Tell me: what are the rules? What must always be true?
31
+ What can never happen?"
32
+ ```
33
+
34
+ These invariants drive the entire model design.
35
+
36
+ ## Subdomain-Aware Modeling Depth
37
+
38
+ Before choosing tactical patterns, decide *how much* DDD this Bounded Context
39
+ deserves. Not every context earns a rich domain model — over-modeling a
40
+ commodity capability is as wasteful as under-modeling a core one.
41
+
42
+ Classify the context (recorded as **Subdomain Type** in
43
+ `dflow/specs/domain/context-map.md`). The discriminator: **"if you replaced
44
+ this with an off-the-shelf SaaS / package, would the system's differentiation
45
+ disappear?"** (For internal systems "differentiation" means a unique
46
+ operational advantage or mission outcome, not only competitive edge.)
47
+
48
+ - **Core** — yes, it would disappear. This is the differentiator.
49
+ - **Supporting** — no, but it still needs customization to your process.
50
+ Necessary, often customized, not a differentiator.
51
+ - **Generic** — no, and off-the-shelf options exist (auth, notifications,
52
+ file upload, audit log are common examples).
53
+
54
+ Rule of thumb against inflation: most contexts are *supporting*; *core* is
55
+ usually only one or two. If everything is core, you haven't classified.
56
+
57
+ Modeling depth follows the type:
58
+
59
+ | Subdomain | Modeling depth | aggregate-design worksheet | Domain Events |
60
+ |---|---|---|---|
61
+ | core | **Full** — rich model, full invariants, the patterns below | required for a new Aggregate | full catalog |
62
+ | supporting | **Standard** — a sufficient model, avoid speculative abstraction | created but kept lean for a new Aggregate | events that are actually needed |
63
+ | generic | **Minimal** — thin wrapper / CRUD / off-the-shelf package behind an interface (ACL seam) | **not created** | usually none (adapter-boundary events excepted) |
64
+
65
+ **Precedence.** This subdomain-aware depth *refines / caps* the structural
66
+ DDD Modeling Depth in `AI-AGENT-GUIDE.md` § Ceremony Scaling and
67
+ `_conventions.md`: when the structural rule says "new Aggregate / BC → Full"
68
+ but the subdomain says Standard / Minimal, follow the **shallower sufficient
69
+ depth** — unless the developer explicitly chooses to model deeper and records
70
+ why (in the context-map Notes or the `aggregate-design.md` Design Decisions).
71
+
72
+ **What "shallower" does NOT mean** — three boundaries the classification must
73
+ never cross:
74
+
75
+ 1. **Business rules are not exempt.** A generic context's rules still get a
76
+ BR-ID in `rules.md` and a scenario in `behavior.md`; `/dflow:verify` works
77
+ the same. What shrinks is the *domain model's thickness*, not the spec's
78
+ existence.
79
+ 2. **The Tier system is unchanged.** A bug fix in a generic context is still a
80
+ T2; a new feature still runs the flow. Subdomain governs *how deep to model
81
+ a context*; Tier governs *how much ceremony a single change needs* — they
82
+ are orthogonal.
83
+ 3. **Criticality is not downgraded.** Security, tests, SLA, audit,
84
+ permissions, and data integrity do **not** drop with the subdomain type —
85
+ auth or notifications can be generic *and* mission-critical. Generic lowers
86
+ the modeling investment, nothing else.
87
+
88
+ ## Pattern Selection Flowchart
89
+
90
+ ```
91
+ Is it identified by an ID that persists over time?
92
+ │
93
+ ├─ Yes → Is it the "boss" that protects a consistency boundary?
94
+ │ ├─ Yes → AGGREGATE ROOT
95
+ │ └─ No → ENTITY (belongs inside an Aggregate)
96
+ │
97
+ └─ No → Is it defined entirely by its properties?
98
+ ├─ Yes → VALUE OBJECT
99
+ └─ No → Does it represent an operation spanning multiple Aggregates?
100
+ ├─ Yes → DOMAIN SERVICE
101
+ └─ No → Re-examine — it's probably one of the above
102
+ ```
103
+
104
+ ## Aggregate Design
105
+
106
+ Aggregates are the most important and most commonly misunderstood DDD concept.
107
+
108
+ ### What is an Aggregate?
109
+
110
+ A cluster of objects treated as a single unit for data changes. The Aggregate Root
111
+ is the only entry point — outside code cannot reach inside and modify child entities
112
+ or value objects directly.
113
+
114
+ ### Aggregate Design Rules
115
+
116
+ 1. **Protect invariants** — The Aggregate exists to enforce business rules that span
117
+ multiple objects within it.
118
+
119
+ 2. **One Aggregate per transaction** — A single operation should modify only ONE
120
+ Aggregate. If you need to modify two Aggregates, use Domain Events for eventual
121
+ consistency.
122
+
123
+ 3. **Reference other Aggregates by ID only** — Never hold a direct object reference
124
+ to another Aggregate. Store its ID instead.
125
+
126
+ 4. **Keep them small** — Large Aggregates cause concurrency issues. If two users can
127
+ independently modify different parts, those parts should probably be separate Aggregates.
128
+ Watch the time axis too: a child collection that grows without bound over the
129
+ Aggregate's lifetime (audit trail, comments, history) makes every load heavier and
130
+ every save more contended — split it out and reference by ID.
131
+
132
+ ### Example: Expense Report Aggregate
133
+
134
+ ```csharp
135
+ // ExpenseReport is the Aggregate Root
136
+ public class ExpenseReport : AggregateRoot
137
+ {
138
+ private readonly List<ExpenseLineItem> _lineItems = new();
139
+
140
+ public EmployeeId SubmittedBy { get; private set; } // Reference by ID
141
+ public ReportPeriod Period { get; private set; } // Value Object
142
+ public Money TotalAmount => CalculateTotal(); // Derived
143
+ public ReportStatus Status { get; private set; } // Value Object (enum-like)
144
+
145
+ // State change through explicit methods — not property setters
146
+ public void AddLineItem(string description, Money amount, ExpenseCategory category)
147
+ {
148
+ // Enforce invariants
149
+ if (Status != ReportStatus.Draft)
150
+ throw new DomainException("Cannot add items to a submitted report.");
151
+
152
+ if (_lineItems.Count >= 50)
153
+ throw new DomainException("Maximum 50 line items per report.");
154
+
155
+ var lineItem = new ExpenseLineItem(description, amount, category);
156
+ _lineItems.Add(lineItem);
157
+
158
+ // Raise domain event
159
+ AddDomainEvent(new LineItemAddedEvent(Id, lineItem.Id, amount));
160
+ }
161
+
162
+ public void Submit()
163
+ {
164
+ if (Status != ReportStatus.Draft)
165
+ throw new DomainException("Only draft reports can be submitted.");
166
+
167
+ if (!_lineItems.Any())
168
+ throw new DomainException("Cannot submit an empty report.");
169
+
170
+ Status = ReportStatus.Submitted;
171
+ AddDomainEvent(new ExpenseReportSubmittedEvent(Id, SubmittedBy, TotalAmount));
172
+ }
173
+ }
174
+ ```
175
+
176
+ ### Design Questions to Ask
177
+
178
+ When designing a new Aggregate:
179
+
180
+ 1. **What are the invariants?** What business rules must ALWAYS be true?
181
+ (Classify each one — see "Invariant Classification" below.)
182
+ 2. **What's the consistency boundary?** What must be updated atomically?
183
+ 3. **What can change independently?** Separate Aggregates for separate concerns.
184
+ 4. **Who modifies this?** How many concurrent users? (Affects Aggregate size)
185
+ 5. **What events does this produce?** What other parts of the system need to know?
186
+ 6. **Will any child collection grow without bound over time?** (history,
187
+ comments, audit entries) Unbounded growth is a split signal — move it to
188
+ its own Aggregate or a read model and reference by ID.
189
+
190
+ ### Invariant Classification
191
+
192
+ Not every `if (…) throw` is an Aggregate invariant. Before adding a rule to an
193
+ Aggregate design, ask: **how many objects' state must you see at once to check
194
+ this rule?**
195
+
196
+ 1. **Local constraint (VO / Entity invariant)** — checkable inside a single
197
+ object. Route it by nature:
198
+ - Pure input shape (required, format, max length) → request validation in
199
+ the Application layer (e.g. a Validator).
200
+ - Domain meaning, even within one object → enforce it in the Value Object /
201
+ Entity constructor or method. `DateRange` rejecting `Start > End` (see
202
+ the Value Objects section below) is a **domain invariant**, not input
203
+ validation — it lives in the VO so an invalid range can never exist.
204
+ 2. **Aggregate boundary invariant** — requires the state of **multiple objects
205
+ inside one Aggregate instance** ("total across all line items must not
206
+ exceed the limit", "cannot submit an empty report"). These are the reason
207
+ the boundary exists — they are the main entries of an `aggregate-design.md`
208
+ Invariants table.
209
+ 3. **Set-based invariant** — spans **multiple Aggregate instances** selected
210
+ by a key or status ("email unique across all Users", "one active session
211
+ per connector"). The Aggregate alone can never enforce it; list it tagged
212
+ `set-based` with its store-level guard named, and see "Set-Based /
213
+ Uniqueness Invariants" below.
214
+
215
+ Mis-filing level 1 rules into the Invariants table dilutes it — reviewers can
216
+ no longer see why the boundary exists. Missing the store-level guard on a
217
+ level 3 rule ships a race condition.
218
+
219
+ ### Set-Based / Uniqueness Invariants
220
+
221
+ Some invariants are not about one Aggregate but about a **set selected by a
222
+ business key or status**: "email (normalized) is unique across all Users", "each
223
+ seat holds at most one active booking", "each connector has at most one
224
+ in-progress charging session". A single Aggregate instance cannot see the rest of
225
+ that set, so it cannot enforce the rule on its own.
226
+
227
+ Handle them the same way **regardless of which Aggregate boundary you choose**:
228
+
229
+ 1. **As separate Aggregates** (e.g. `User` and `Booking` are distinct): the
230
+ Application layer *orchestrates* the check — via a repository query, a
231
+ Specification, or a domain service (the rule stays domain-named; it is not an
232
+ inline `if-else` in the command handler) — and the **database enforces it with
233
+ a unique / partial (filtered) unique index**. The DB constraint is the real
234
+ guarantee under concurrency; the orchestrated check just returns a friendlier
235
+ error first.
236
+
237
+ 2. **Folded into one Aggregate** (e.g. the active session lives *inside* a
238
+ `Connector` as a child entity): the in-memory check (`if (Status == InUse)
239
+ throw …`) is logically correct, **but is still not concurrency-safe by
240
+ itself**. Two concurrent commands can each load the Aggregate, both pass the
241
+ check, and both save. Close the race with **optimistic concurrency** (a
242
+ `rowversion` / version token on the Aggregate root) or a DB constraint — but
243
+ the version check only protects you if every save actually touches the root's
244
+ token: inserting a child row without bumping the root's version leaves the
245
+ race open. Translate the resulting concurrency exception / unique violation
246
+ into a meaningful business conflict (e.g. HTTP 409), not a generic 500.
247
+
248
+ **Key point:** an in-memory check — at *any* layer — is never the final guarantee
249
+ for a uniqueness / "only one active X" rule under concurrent requests. The durable
250
+ enforcement is a DB unique / filtered index, an optimistic-concurrency token, or
251
+ an equivalent conditional write / compare-and-swap. Pick the Aggregate boundary on
252
+ modeling grounds (does the inner thing have an independent lifecycle / history
253
+ worth querying?), then add the store-level guard either way. (Heavier
254
+ serialization tactics — distributed locks, per-key actors, aggregate-per-key
255
+ sharding — exist but are advanced; reach for a store-level constraint or version
256
+ check first.)
257
+
258
+ ## Value Objects
259
+
260
+ ### When to Use Value Objects
261
+
262
+ If the answer to ALL of these is "yes", it's a Value Object:
263
+ - Is it defined by its properties, not by an ID?
264
+ - Is it immutable once created?
265
+ - Can two instances with the same properties be considered equal?
266
+
267
+ ### Common Value Objects
268
+
269
+ ```csharp
270
+ // Money — the classic example
271
+ public record Money(decimal Amount, Currency Currency)
272
+ {
273
+ public static Money Zero(Currency currency) => new(0, currency);
274
+
275
+ public Money Add(Money other)
276
+ {
277
+ if (Currency != other.Currency)
278
+ throw new CurrencyMismatchException(Currency, other.Currency);
279
+ return new Money(Amount + other.Amount, Currency);
280
+ }
281
+
282
+ public Money ConvertTo(Currency target, ExchangeRate rate)
283
+ {
284
+ return new Money(rate.Convert(Amount), target);
285
+ }
286
+ }
287
+
288
+ // DateRange
289
+ public record DateRange(DateOnly Start, DateOnly End)
290
+ {
291
+ public DateRange
292
+ {
293
+ if (Start > End) throw new DomainException("Start must be before End.");
294
+ }
295
+
296
+ public bool Contains(DateOnly date) => date >= Start && date <= End;
297
+ public int Days => End.DayNumber - Start.DayNumber + 1;
298
+ }
299
+
300
+ // Currency (constrained string)
301
+ public record Currency
302
+ {
303
+ public string Code { get; }
304
+ public int DecimalPlaces { get; }
305
+
306
+ public static readonly Currency TWD = new("TWD", 0);
307
+ public static readonly Currency USD = new("USD", 2);
308
+ public static readonly Currency JPY = new("JPY", 0);
309
+
310
+ private Currency(string code, int decimalPlaces)
311
+ {
312
+ Code = code;
313
+ DecimalPlaces = decimalPlaces;
314
+ }
315
+
316
+ public decimal Round(decimal amount) =>
317
+ Math.Round(amount, DecimalPlaces, MidpointRounding.AwayFromZero);
318
+ }
319
+ ```
320
+
321
+ ### Value Object Design Questions
322
+
323
+ 1. **Does it have behavior?** Good VOs have methods, not just properties.
324
+ 2. **Does it enforce constraints?** Constructor should reject invalid states.
325
+ 3. **Is it reusable?** `Money` can be used across many Aggregates.
326
+
327
+ ### Strong-Typed IDs and Domain Primitives
328
+
329
+ **IDs should be strong-typed.** Use a typed id (`ExpenseReportId`, `EmployeeId`
330
+ — the examples above already do) instead of a raw `Guid` / `int` / `string`, so
331
+ the compiler rejects passing one Aggregate's id where another's is expected.
332
+
333
+ ```csharp
334
+ // Raw: both are Guid — swap them and the compiler stays silent
335
+ void Transfer(Guid from, Guid to, Money amount)
336
+
337
+ // Typed: passing a CustomerId where an AccountId is expected won't compile
338
+ void Transfer(AccountId from, AccountId to, Money amount)
339
+ ```
340
+
341
+ A typed id is a **Value Object wrapping the identifier value** — a
342
+ value-by-identity special case — not the Entity it identifies. (So it does not
343
+ contradict the Pattern Selection flowchart's "identified by an ID → Entity":
344
+ that question is about the *concept*; this is about giving the *id value* a type.)
345
+
346
+ **For other primitives, wrap only when the value carries a rule** — a format
347
+ (`Email`), a unit (`Money`, `Weight`), a constrained comparison / value semantic
348
+ (`DateRange` with start ≤ end), or an **allowed value set / constrained
349
+ vocabulary / domain code** (the `Currency` constrained string and the
350
+ `ReportStatus` enum-like value above are exactly this). A plain rule-less
351
+ `string` / `int` should **not** be wrapped — that is over-engineering. The point
352
+ of wrapping a primitive is to **bind a rule to the type**; with no rule, there is
353
+ no reason to wrap.
354
+
355
+ ## Domain Events
356
+
357
+ ### What Are Domain Events?
358
+
359
+ Something that happened in the domain that other parts of the system care about.
360
+ Past tense naming: `ExpenseReportSubmitted`, `LineItemAdded`, `ReportApproved`.
361
+
362
+ > **Not event sourcing.** Dflow's Domain Events are state-change
363
+ > *notifications* — the Aggregate's persisted state stays the source of truth,
364
+ > and events are never replayed to rebuild it. Adopting event sourcing is a
365
+ > separate, heavyweight architecture decision: if genuinely needed, record it
366
+ > as an ADR (`dflow/specs/architecture/decisions/`; Brownfield — see the
367
+ > Edition note); never introduce it as a side effect of modeling.
368
+
369
+ ### When to Use Domain Events
370
+
371
+ - When one Aggregate needs to trigger changes in another Aggregate
372
+ - When side effects (email, notification, audit log) should happen after a domain action
373
+ - When different Bounded Contexts need to communicate
374
+
375
+ ### Event Design
376
+
377
+ ```csharp
378
+ public record ExpenseReportSubmittedEvent(
379
+ ExpenseReportId ReportId,
380
+ EmployeeId SubmittedBy,
381
+ Money TotalAmount
382
+ ) : IDomainEvent;
383
+ ```
384
+
385
+ ### Payload Guideline
386
+
387
+ Default to a **thin payload**: the IDs involved, the fact that happened, and
388
+ the values that changed — not a snapshot of the whole Aggregate.
389
+
390
+ - Same-context consumers that need more state should load it by ID — a fat
391
+ payload is a stale copy the moment it is published.
392
+ - Across Bounded Contexts, every field you publish is something downstream may
393
+ start depending on — a fat payload becomes an implicit contract. If a
394
+ consumer genuinely needs state transfer, make that an explicit **integration
395
+ event** decision (record the Delivery expectation in `events.md`; Brownfield —
396
+ see the Edition note) and treat
397
+ the payload as a published contract: add fields if you must, never change
398
+ their meaning. Ceremony-wise, extending an event payload that crosses
399
+ contexts is a T1 contract change (see the ceremony examples in
400
+ `_conventions.md`).
401
+
402
+ ### Event Flow
403
+
404
+ ```
405
+ 1. Aggregate method called → state changes → event added to DomainEvents list
406
+ 2. Repository saves Aggregate
407
+ 3. After save (in same transaction or via outbox):
408
+ - In-process handlers: update read models, trigger other commands
409
+ - Cross-context: publish to message queue
410
+ ```
411
+
412
+ ### Event Handling Guidelines
413
+
414
+ - **Same Bounded Context**: Handle synchronously (same transaction OK for read models)
415
+ - **Cross Bounded Context**: Handle asynchronously (eventual consistency)
416
+ - Event handlers should be idempotent (safe to process multiple times)
417
+ - **The failure path is a business scenario, not a technical detail**: when a
418
+ cross-Aggregate chain is async (eventually consistent) **and** a handler's
419
+ final failure — after retries are exhausted — would be business-visible
420
+ (compensation, customer entitlements, money, inventory, compliance, manual
421
+ reconciliation), write that outcome as a `BR-*` with its own Given/When/Then
422
+ in `behavior.md` and list the failure as an `EC-*` edge case in the spec —
423
+ even if the documented answer is "ops reconciles manually". Best-effort side
424
+ effects (notification email, logging) don't need a BR; a line in `events.md`
425
+ Event Flow Notes or a tech-debt entry is enough.
426
+ - **Dispatch / clear lifecycle**: clear `DomainEvents` at the Repository / Unit
427
+ of Work **save boundary** — the UoW clears them *after* the save succeeds and
428
+ the events have been dispatched (or the outbox row persisted, or dispatch
429
+ explicitly decided to be dropped/deferred). Never clear inside an Aggregate
430
+ method, and never before the save succeeds. A simple in-process dispatcher
431
+ (e.g. `IPublisher` / MediatR) is enough for Phase 1; an **outbox /
432
+ integration-event bridge is the Phase 2+ upgrade** for reliable cross-service
433
+ delivery. If no dispatcher is wired yet, record it as deferred tech debt — but
434
+ still clear on a successful save so events cannot accumulate unbounded.
435
+
436
+ ## Specifications
437
+
438
+ For complex query logic that belongs to the domain:
439
+
440
+ ```csharp
441
+ public class PendingApprovalSpec : Specification<ExpenseReport>
442
+ {
443
+ private readonly EmployeeId _approverId;
444
+
445
+ public PendingApprovalSpec(EmployeeId approverId)
446
+ {
447
+ _approverId = approverId;
448
+ }
449
+
450
+ public override Expression<Func<ExpenseReport, bool>> ToExpression()
451
+ {
452
+ return report =>
453
+ report.Status == ReportStatus.Submitted &&
454
+ report.ApproverId == _approverId;
455
+ }
456
+ }
457
+ ```
458
+
459
+ ## Read Models (Query Side)
460
+
461
+ The **write side** goes through an Aggregate (to protect invariants). The
462
+ **read side** usually should not. Lists, reports, dashboards, dropdowns — work
463
+ that only *displays* data — can query a denormalized projection / DTO directly,
464
+ without loading an Aggregate and without going through the Domain layer.
465
+
466
+ ```csharp
467
+ // Don't: load 50 full Order Aggregates (each pulling line items, status
468
+ // history, …) just to render an order list → object-graph bloat, N+1
469
+ var orders = orderRepository.GetAll();
470
+
471
+ // Do: project only the columns the screen needs straight into a DTO
472
+ // SELECT OrderNumber, CustomerName, Total, OrderDate FROM Orders
473
+ // → IReadOnlyList<OrderListItemDto>
474
+ ```
475
+
476
+ **When to still go through the Aggregate**: when you read in order to *change*
477
+ (read-modify-write). "Cancel order #123" must enforce "cannot cancel a shipped
478
+ order" → load the `Order` Aggregate, call `order.Cancel()`, save. Pure display →
479
+ read model; about to mutate → Aggregate.
480
+
481
+ **Read Model vs Specification**: a `Specification` is a domain predicate /
482
+ write-side decision — it carries a rule (see "Specifications" above). A read
483
+ model is a presentation / reporting query shape that carries **no** invariant.
484
+ Don't conflate them.
485
+
486
+ **Keep it simple.** This is the query side of CQRS — it does **not** require
487
+ event sourcing or a separate database. Two common shapes:
488
+
489
+ - A **live query DTO against the same database** — always fresh; the simplest
490
+ and most common read model.
491
+ - An **event-updated denormalized table** — can be **stale**. If that staleness
492
+ is user-visible (e.g. a just-placed order missing from the list for a moment),
493
+ say so in `behavior.md` / an edge case / a design decision. This is the same
494
+ mechanism as the "same-transaction read model update" note under Event
495
+ Handling Guidelines.
496
+
497
+ A read model is a *simplification* of the read path, not an extra layer to
498
+ build. (Read model ≠ event sourcing.)
499
+
500
+ ## Domain Services
501
+
502
+ Use Domain Services for operations that:
503
+ - Involve multiple Aggregates (read-only access to the second Aggregate)
504
+ - Require external information (through interfaces) to make domain decisions
505
+ - Don't naturally belong to any single Entity
506
+
507
+ ```csharp
508
+ // Domain Service — in Domain layer
509
+ public class ExpenseApprovalService
510
+ {
511
+ private readonly IApprovalPolicyRepository _policyRepo;
512
+
513
+ public ApprovalResult Evaluate(ExpenseReport report, ApprovalPolicy policy)
514
+ {
515
+ if (report.TotalAmount.Amount > policy.AutoApprovalLimit)
516
+ return ApprovalResult.RequiresManagerApproval;
517
+
518
+ if (policy.RestrictedCategories.Overlaps(report.Categories))
519
+ return ApprovalResult.RequiresComplianceReview;
520
+
521
+ return ApprovalResult.AutoApproved;
522
+ }
523
+ }
524
+ ```
525
+
526
+ ## Factories
527
+
528
+ When *creating* an object is itself complex — many parts to assemble, an
529
+ implementation type to choose, or creation rules that span several objects —
530
+ don't cram it into the constructor. Pick the lightest tool that fits:
531
+
532
+ - **Simple construction → the constructor.** A `new` whose constructor enforces
533
+ the object's own invariants is enough. Don't add a factory just to have one.
534
+ - **Creating a child entity / needs the Aggregate's internal state → a factory
535
+ method on the Aggregate Root** (e.g. `order.AddLine(...)` — the root stays in
536
+ control of its invariants).
537
+ - **Complex creation across several objects / choosing an implementation type →
538
+ a standalone Factory** (or a Domain Service).
539
+
540
+ ### Reconstitution ≠ creation
541
+
542
+ The blind spot is the other half of an object's lifecycle: **rebuilding an
543
+ existing object from persistence is not creating a new one.** Reconstitution
544
+ must **not** run the creation workflow, **not** re-raise creation events, and
545
+ **not** reject old data just because a *new* creation rule was added later.
546
+
547
+ ```csharp
548
+ // Creation: this Order comes into existence for the first time
549
+ var order = Order.Create(customerId, items);
550
+ // → assigns a new OrderId, checks "at least one line",
551
+ // raises OrderPlaced (which sends mail, decrements stock)
552
+
553
+ // Reconstitution: the same Order loaded back next week
554
+ var order = repository.GetById(orderId);
555
+ // → must NOT raise OrderPlaced again (mail re-sent, stock re-decremented)
556
+ // → must NOT be rejected by a creation rule added after it was placed
557
+ ```
558
+
559
+ ORMs reconstitute through a backing / `private` constructor; reserve the
560
+ `Create(...)` factory path for genuinely new instances.
561
+
562
+ **But "reconstitution validates nothing" is the wrong reading.** Structural /
563
+ version-compatibility invariants can and should still be checked on hydration;
564
+ genuinely corrupt or legacy-incompatible data should be routed to migration or
565
+ quarantine, not silently accepted. What you drop is *re-running the creation
566
+ workflow and its side effects* — not *integrity*.
567
+
568
+ ## Bounded Context Relationships
569
+
570
+ If `dflow/specs/domain/context-map.md` does not exist yet, create it from `templates/context-map.md` before documenting relationships.
571
+
572
+ Document in `dflow/specs/domain/context-map.md`:
573
+
574
+ | Relationship | Pattern | Example |
575
+ |---|---|---|
576
+ | Context A calls Context B | Customer-Supplier | Expense → HR (get employee info) |
577
+ | Contexts share data | Shared Kernel | Currency, Money in SharedKernel/ |
578
+ | Context A translates B's language | Anti-Corruption Layer | Expense → External Accounting System |
579
+ | Fire and forget | Domain Events | Expense → Notification (report submitted) |
580
+ | Two contexts have no compelling reason to integrate | Separate Ways | Shipping vs Marketing each keep their own product fields |
581
+ | Integrating with an unmodeled legacy region | Big Ball of Mud (+ ACL) | a new Order context wrapping a legacy accounting module |
582
+
583
+ The less-obvious three — when to reach for them:
584
+
585
+ - **Separate Ways** — when integration cost > duplication cost **and** the "shared"
586
+ concept isn't actually the same thing in both contexts: don't build an ACL or a
587
+ Shared Kernel, just duplicate the small overlap; revisit only if the duplication
588
+ painfully diverges later. (Counters "integrate because DDD".) **Do NOT** use
589
+ Separate Ways when one context genuinely needs the other's authoritative
590
+ lifecycle, fresh state, workflow coordination, or invariant protection — that is
591
+ required integration, not a duplication candidate.
592
+ - **Big Ball of Mud (+ ACL)** — when a region has no coherent model (often legacy)
593
+ and you must integrate with it: mark it `BBoM` on the context map, wrap it behind
594
+ an Anti-Corruption Layer, and translate at the boundary so its (lack of) structure
595
+ can't leak into your clean contexts — don't try to model the mud in place. In
596
+ Brownfield extraction, the legacy you are pulling away from is usually this.
597
+ - **Open Host Service (OHS)** — when **many** downstream contexts need the same
598
+ integration from one upstream, publish a single Open Host Service with a Published
599
+ Language (a documented, stable protocol) instead of each consumer integrating
600
+ separately. OHS standardizes the upstream's published surface; it does **not**
601
+ remove a consumer-side ACL where that consumer still needs local translation.
602
+
603
+ ## Common Mistakes to Catch
604
+
605
+ 1. **Anemic Domain Model** — Entities with only getters/setters, all logic in services
606
+ → Move behavior INTO the Entity/Aggregate
607
+
608
+ 2. **Too-large Aggregates** — Aggregate that loads entire object graph
609
+ → Split into smaller Aggregates, reference by ID
610
+
611
+ 3. **Business logic in Application layer** — If-else rules in command handlers
612
+ → Move to Domain (Entity methods or Domain Services)
613
+
614
+ 4. **Business logic in Infrastructure** — Rules in SQL queries or EF configurations
615
+ → Domain defines WHAT, Infrastructure defines HOW
616
+
617
+ 5. **Direct cross-Aggregate modification** — One command modifying two Aggregates
618
+ → Use Domain Events for the second Aggregate
619
+
620
+ 6. **Set-based invariant guarded only in memory** — A "unique" / "only one active"
621
+ rule enforced solely by an in-app check, with no DB unique constraint or
622
+ optimistic-concurrency token → two concurrent requests can both pass and break it
623
+ → Back it with a DB constraint / `rowversion`; see "Set-Based / Uniqueness Invariants"
624
+
625
+ 7. **Unbounded collection inside an Aggregate** — A child list that grows forever
626
+ (history, comments, audit trail) makes every load heavier and every save more
627
+ contended → Split it into its own Aggregate or a read model, reference by ID
628
+
629
+ 8. **Fat event payload as implicit contract** — Publishing whole-Aggregate
630
+ snapshots couples consumers to your internal shape and hands them stale data
631
+ → Default to IDs + event facts + changed values; cross-context state transfer
632
+ must be an explicit integration-event contract (see "Payload Guideline")
633
+
634
+ 9. **Creation validation / events re-run on reconstitution** — Loading an
635
+ existing Aggregate from the database through the creation path re-raises
636
+ creation events (re-sends mail, re-decrements stock) or rejects valid old data
637
+ by a newer creation rule → Reconstitute through a backing constructor; keep
638
+ integrity checks but not the creation workflow; see "Reconstitution ≠ creation"
639
+
640
+ 10. **Forcing every query through repository + Aggregate** — Loading full
641
+ Aggregates just to display a list / report causes object-graph bloat and N+1
642
+ → Use a read model (denormalized projection / DTO) for the display path; see
643
+ "Read Models (Query Side)"
@@ -5,12 +5,15 @@ description: >
5
5
  /dflow:* commands (/dflow:new-feature, /dflow:modify-existing, /dflow:bug-fix,
6
6
  /dflow:new-phase, /dflow:finish-feature, /dflow:pr-review, /dflow:verify,
7
7
  /dflow:report-dflow-feedback, /dflow:status, /dflow:next, /dflow:cancel).
8
- SECONDARY (auto-trigger safety net) — engage ONLY for: adding or changing
9
- product/domain behavior, new requirements, a feature or bug-fix workflow, or
10
- spec-impacting architecture/domain-model decisions. Do NOT engage for pure
11
- refactors, infrastructure chores, formatting, or general code questions.
12
- When engaged by natural language, DO NOT auto-enter a workflow: judge the
13
- intent, suggest the matching /dflow: command, and wait for confirmation.
8
+ SECONDARY — engage ONLY for adding or changing product/user-facing/domain
9
+ behavior, a new requirement, a feature or bug-fix workflow, or spec-impacting
10
+ architecture/domain-model decisions. Includes indirect phrasings, e.g. "I want
11
+ to build/add ...", "let's add the ability to ...", "we need to support ...",
12
+ "can you implement ...", "users should be able to ...", "the app should also
13
+ ...". Do NOT engage for pure refactors, renames, infra/build chores,
14
+ formatting, dep bumps, or general code questions ("how does X work", "explain
15
+ this"). When engaged by natural language, DO NOT auto-enter a workflow: judge
16
+ the intent, suggest the matching /dflow: command, and wait for confirmation.
14
17
  ---
15
18
 
16
19
  <!-- dflow-generated: skill-adapter -->