@jarrodmedrano/claude-skills 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/bevy/SKILL.md +406 -0
- package/.claude/skills/bevy/references/bevy_specific_tips.md +385 -0
- package/.claude/skills/bevy/references/common_pitfalls.md +217 -0
- package/.claude/skills/bevy/references/ecs_patterns.md +277 -0
- package/.claude/skills/bevy/references/project_structure.md +116 -0
- package/.claude/skills/bevy/references/ui_development.md +147 -0
- package/.claude/skills/domain-driven-design/SKILL.md +459 -0
- package/.claude/skills/domain-driven-design/references/ddd_foundations_and_patterns.md +664 -0
- package/.claude/skills/domain-driven-design/references/rich_hickey_principles.md +406 -0
- package/.claude/skills/domain-driven-design/references/visualization_examples.md +790 -0
- package/.claude/skills/domain-driven-design/references/wlaschin_patterns.md +639 -0
- package/.claude/skills/godot/SKILL.md +728 -0
- package/.claude/skills/godot/assets/templates/attribute_template.gd +109 -0
- package/.claude/skills/godot/assets/templates/component_template.gd +76 -0
- package/.claude/skills/godot/assets/templates/interaction_template.gd +108 -0
- package/.claude/skills/godot/assets/templates/item_resource.tres +11 -0
- package/.claude/skills/godot/assets/templates/spell_resource.tres +20 -0
- package/.claude/skills/godot/references/architecture-patterns.md +608 -0
- package/.claude/skills/godot/references/common-pitfalls.md +518 -0
- package/.claude/skills/godot/references/file-formats.md +491 -0
- package/.claude/skills/godot/references/godot4-physics-api.md +302 -0
- package/.claude/skills/godot/scripts/validate_tres.py +145 -0
- package/.claude/skills/godot/scripts/validate_tscn.py +170 -0
- package/.claude/skills/guitar-fretboard-mastery/SKILL.md +179 -0
- package/.claude/skills/guitar-fretboard-mastery/guitar-fretboard-mastery.skill +0 -0
- package/.claude/skills/react-three-fiber/SKILL.md +2055 -0
- package/.claude/skills/react-three-fiber/scripts/build-scene.ts +171 -0
- package/package.json +1 -1
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
# DDD Foundations and Practical Patterns
|
|
2
|
+
|
|
3
|
+
This reference combines Eric Evans' foundational Domain-Driven Design concepts with practical patterns from Clojure and functional programming approaches, plus Martin Fowler's guidance on Ubiquitous Language.
|
|
4
|
+
|
|
5
|
+
## Eric Evans' Core DDD Concepts
|
|
6
|
+
|
|
7
|
+
### Ubiquitous Language (Fowler & Evans)
|
|
8
|
+
|
|
9
|
+
**Definition:** A language structured around the domain model and used by all team members to connect all activities of the team with the software.
|
|
10
|
+
|
|
11
|
+
**Key Principles:**
|
|
12
|
+
- Same language in conversations, documentation, diagrams, and code
|
|
13
|
+
- Developed through collaboration between developers and domain experts
|
|
14
|
+
- Evolves with the model; changes in language reflect changes in understanding
|
|
15
|
+
- No translation between business and technical discussions
|
|
16
|
+
|
|
17
|
+
**Building Ubiquitous Language:**
|
|
18
|
+
1. Listen to how domain experts speak
|
|
19
|
+
2. Extract nouns (concepts) and verbs (operations)
|
|
20
|
+
3. Challenge vague terms - ask for precision
|
|
21
|
+
4. Document terms in a glossary
|
|
22
|
+
5. Use these exact terms in code - class names, function names, module names
|
|
23
|
+
6. When language feels awkward in code, it reveals model problems
|
|
24
|
+
|
|
25
|
+
**Red Flags:**
|
|
26
|
+
- Developers use different terms than domain experts
|
|
27
|
+
- Translation happens between "business language" and "technical language"
|
|
28
|
+
- Terms have different meanings in different contexts (without bounded contexts)
|
|
29
|
+
- Code uses generic names like "Manager", "Handler", "Processor" instead of domain terms
|
|
30
|
+
|
|
31
|
+
**Example - Good Ubiquitous Language:**
|
|
32
|
+
```clojure
|
|
33
|
+
;; Domain expert: "We post a transfer to debit one account and credit another"
|
|
34
|
+
(defn post-transfer [transfer-number debit credit]
|
|
35
|
+
...)
|
|
36
|
+
|
|
37
|
+
;; NOT:
|
|
38
|
+
(defn create-transaction [id source dest] ; "transaction" is technical jargon
|
|
39
|
+
...)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Entities
|
|
43
|
+
|
|
44
|
+
**Definition:** An object defined primarily by its identity, rather than its attributes.
|
|
45
|
+
|
|
46
|
+
**Characteristics:**
|
|
47
|
+
- Has a unique identifier
|
|
48
|
+
- Identity persists through time
|
|
49
|
+
- Attributes may change while identity remains constant
|
|
50
|
+
- Two entities with same attributes but different IDs are distinct
|
|
51
|
+
|
|
52
|
+
**When to Use:**
|
|
53
|
+
- Domain experts refer to things by name/ID
|
|
54
|
+
- The thing continues to exist even as its properties change
|
|
55
|
+
- Need to track the thing over time or across system boundaries
|
|
56
|
+
|
|
57
|
+
**Clojure Pattern:**
|
|
58
|
+
```clojure
|
|
59
|
+
;; Entity: Account (identity = account number)
|
|
60
|
+
(s/def :account/number
|
|
61
|
+
(s/and string? #(re-matches #"[1-9]{12}" %)))
|
|
62
|
+
|
|
63
|
+
(s/def :account/account
|
|
64
|
+
(s/keys :req-un [:account/number ; The identity
|
|
65
|
+
:balance/balance])) ; Mutable state
|
|
66
|
+
|
|
67
|
+
(defn make-account [account-number balance]
|
|
68
|
+
(s/assert :account/account
|
|
69
|
+
{:number account-number
|
|
70
|
+
:balance balance}))
|
|
71
|
+
|
|
72
|
+
;; Same account, even as balance changes:
|
|
73
|
+
(def account-1 (make-account "123456789012" (make-balance 1000 :usd)))
|
|
74
|
+
(def account-2 (make-account "123456789012" (make-balance 500 :usd)))
|
|
75
|
+
;; account-1 and account-2 represent the same account entity
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Immutable Entities:**
|
|
79
|
+
Not all entities are mutable. Some are created once and never change:
|
|
80
|
+
|
|
81
|
+
```clojure
|
|
82
|
+
;; Transfer is immutable but still an entity (has identity: transfer number)
|
|
83
|
+
(s/def :transfer/number
|
|
84
|
+
(s/and string? #(re-matches #"[A-Z]{3}[1-9]{8}" %)))
|
|
85
|
+
|
|
86
|
+
(s/def :transfer/transfer
|
|
87
|
+
(s/keys :req-un [:transfer/id
|
|
88
|
+
:transfer/number ; Identity
|
|
89
|
+
:debit/debit
|
|
90
|
+
:credit/credit
|
|
91
|
+
:transfer/creation-date]))
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Value Objects
|
|
95
|
+
|
|
96
|
+
**Definition:** An object defined entirely by its attributes, with no identity.
|
|
97
|
+
|
|
98
|
+
**Characteristics:**
|
|
99
|
+
- Defined by what it is, not who it is
|
|
100
|
+
- No unique identifier
|
|
101
|
+
- Two value objects with same attributes are equivalent
|
|
102
|
+
- Typically immutable
|
|
103
|
+
- Can be freely shared and replaced
|
|
104
|
+
|
|
105
|
+
**When to Use:**
|
|
106
|
+
- Domain experts describe things by their properties, not by name
|
|
107
|
+
- Identity doesn't matter - only the value matters
|
|
108
|
+
- Can be replaced with an equivalent value without concern
|
|
109
|
+
|
|
110
|
+
**Clojure Pattern:**
|
|
111
|
+
```clojure
|
|
112
|
+
;; Value Object: Amount
|
|
113
|
+
(s/def :amount/currency #{:usd :cad})
|
|
114
|
+
(s/def :amount/value (s/and number? pos?))
|
|
115
|
+
|
|
116
|
+
(s/def :amount/amount
|
|
117
|
+
(s/keys :req-un [:amount/currency
|
|
118
|
+
:amount/value]))
|
|
119
|
+
|
|
120
|
+
(defn make-amount [value currency]
|
|
121
|
+
(s/assert :amount/amount
|
|
122
|
+
{:currency currency
|
|
123
|
+
:value value}))
|
|
124
|
+
|
|
125
|
+
;; Two amounts with same value are equivalent:
|
|
126
|
+
(= (make-amount 100 :usd) (make-amount 100 :usd)) ; => true
|
|
127
|
+
;; No identity - only the value matters
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Entity vs Value Object - Decision Guide:**
|
|
131
|
+
|
|
132
|
+
| Question | Entity | Value Object |
|
|
133
|
+
|----------|--------|--------------|
|
|
134
|
+
| Do domain experts refer to it by ID/name? | Yes | No |
|
|
135
|
+
| Does it change over time? | Often yes | No |
|
|
136
|
+
| Do two instances with same attributes mean the same thing? | No | Yes |
|
|
137
|
+
| Can you replace it with an equivalent copy? | No | Yes |
|
|
138
|
+
|
|
139
|
+
### Aggregates and Aggregate Roots
|
|
140
|
+
|
|
141
|
+
**Definition:** A cluster of domain objects (entities and value objects) that can be treated as a single unit for data changes.
|
|
142
|
+
|
|
143
|
+
**Aggregate Root:** The single entity through which all operations on the aggregate must pass.
|
|
144
|
+
|
|
145
|
+
**Purpose:**
|
|
146
|
+
- Define transactional consistency boundaries
|
|
147
|
+
- Simplify the domain model by grouping related objects
|
|
148
|
+
- Enforce invariants that span multiple objects
|
|
149
|
+
|
|
150
|
+
**Rules:**
|
|
151
|
+
1. **External references go only to the aggregate root**
|
|
152
|
+
- Other systems/aggregates can only hold references to the root
|
|
153
|
+
- Never hold a reference to an internal entity
|
|
154
|
+
|
|
155
|
+
2. **Root enforces all invariants**
|
|
156
|
+
- Only the root can directly change internal entities
|
|
157
|
+
- Internal entities can exist, but external code can't access them directly
|
|
158
|
+
|
|
159
|
+
3. **Delete removes everything**
|
|
160
|
+
- Deleting the root should delete all internal entities
|
|
161
|
+
|
|
162
|
+
4. **Transactions don't span aggregates**
|
|
163
|
+
- One transaction = one aggregate
|
|
164
|
+
- Cross-aggregate consistency is eventual
|
|
165
|
+
|
|
166
|
+
**Example - Order Aggregate:**
|
|
167
|
+
|
|
168
|
+
Traditional OOP approach would have:
|
|
169
|
+
```
|
|
170
|
+
Order (root)
|
|
171
|
+
├─ OrderLine (internal entity)
|
|
172
|
+
├─ ShippingAddress (value object)
|
|
173
|
+
└─ PaymentInfo (value object)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Clojure/Functional Approach:**
|
|
177
|
+
```clojure
|
|
178
|
+
;; In Clojure, aggregates are often just nested data with root-level invariants
|
|
179
|
+
|
|
180
|
+
(s/def ::order-line
|
|
181
|
+
(s/keys :req-un [::product-id ::quantity ::price]))
|
|
182
|
+
|
|
183
|
+
(s/def ::order
|
|
184
|
+
(s/and
|
|
185
|
+
(s/keys :req-un [::order-id ; Root identity
|
|
186
|
+
::customer-id
|
|
187
|
+
::order-lines ; Collection of internal entities
|
|
188
|
+
::shipping-address
|
|
189
|
+
::status])
|
|
190
|
+
;; Aggregate-level invariant:
|
|
191
|
+
(fn [order]
|
|
192
|
+
(and (seq (:order-lines order)) ; Must have at least one line
|
|
193
|
+
(every-price-matches-product (:order-lines order))))))
|
|
194
|
+
|
|
195
|
+
;; Operations go through the root:
|
|
196
|
+
(defn add-order-line [order line]
|
|
197
|
+
;; Add line and validate aggregate invariants
|
|
198
|
+
(let [updated-order (update order :order-lines conj line)]
|
|
199
|
+
(s/assert ::order updated-order)))
|
|
200
|
+
|
|
201
|
+
;; External references by ID only:
|
|
202
|
+
(s/def ::customer-reference
|
|
203
|
+
(s/keys :req-un [::customer-id])) ; Reference to Customer aggregate by ID
|
|
204
|
+
|
|
205
|
+
;; NOT: embedding full customer aggregate
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**When NOT to Use Aggregates:**
|
|
209
|
+
|
|
210
|
+
From the Clojure example:
|
|
211
|
+
```clojure
|
|
212
|
+
;; Transfer involves two accounts, but they don't form an aggregate
|
|
213
|
+
;; because accounts can be modified independently of transfers.
|
|
214
|
+
;; Instead, transfer-money is a domain service.
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Not everything that's related should be an aggregate. Ask:
|
|
218
|
+
- Do these objects need to change together in a single transaction?
|
|
219
|
+
- Do invariants span these objects?
|
|
220
|
+
- Can one exist without the other?
|
|
221
|
+
|
|
222
|
+
**Size Guideline:**
|
|
223
|
+
- Keep aggregates small
|
|
224
|
+
- Prefer smaller aggregates with eventual consistency between them
|
|
225
|
+
- Large aggregates create contention and performance issues
|
|
226
|
+
|
|
227
|
+
### Domain Services
|
|
228
|
+
|
|
229
|
+
**Definition:** Operations that don't naturally belong to any single entity or value object.
|
|
230
|
+
|
|
231
|
+
**When to Use:**
|
|
232
|
+
- Operation involves multiple aggregates or entities
|
|
233
|
+
- Operation doesn't conceptually belong to one object
|
|
234
|
+
- Named after an activity/operation, not a thing
|
|
235
|
+
|
|
236
|
+
**Clojure Pattern:**
|
|
237
|
+
```clojure
|
|
238
|
+
;; Domain Service: transfer-money
|
|
239
|
+
;; Involves: two Account entities + one Transfer entity
|
|
240
|
+
;; Doesn't belong exclusively to any one of them
|
|
241
|
+
|
|
242
|
+
(defn transfer-money
|
|
243
|
+
"Domain service for transferring money between accounts.
|
|
244
|
+
Pure function that returns domain events describing the changes."
|
|
245
|
+
[transfer-number from-account to-account amount]
|
|
246
|
+
(let [debit (dm/make-debit (:number from-account) amount)
|
|
247
|
+
credit (dm/make-credit (:number to-account) amount)
|
|
248
|
+
debited-account (dm/debit-account from-account debit)
|
|
249
|
+
credited-account (dm/credit-account to-account credit)
|
|
250
|
+
posted-transfer (dm/post-transfer transfer-number debit credit)]
|
|
251
|
+
;; Return domain event describing all changes
|
|
252
|
+
{:debited-account debited-account
|
|
253
|
+
:credited-account credited-account
|
|
254
|
+
:posted-transfer posted-transfer}))
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Domain Service vs Application Service:**
|
|
258
|
+
|
|
259
|
+
| Domain Service | Application Service |
|
|
260
|
+
|----------------|---------------------|
|
|
261
|
+
| Pure function | Coordinates effects |
|
|
262
|
+
| Domain logic | Orchestration |
|
|
263
|
+
| Returns events/new states | Persists changes |
|
|
264
|
+
| Part of domain model | Uses domain model |
|
|
265
|
+
| No dependencies on infrastructure | Uses repositories, external services |
|
|
266
|
+
|
|
267
|
+
### Repositories
|
|
268
|
+
|
|
269
|
+
**Definition:** Provides the illusion of an in-memory collection of aggregates, abstracting persistence.
|
|
270
|
+
|
|
271
|
+
**Purpose:**
|
|
272
|
+
- Encapsulate data access logic
|
|
273
|
+
- Provide aggregate-oriented persistence
|
|
274
|
+
- Separate domain model from persistence concerns
|
|
275
|
+
|
|
276
|
+
**Key Characteristics:**
|
|
277
|
+
- Operate at aggregate boundaries (save/load whole aggregates)
|
|
278
|
+
- Provide lookup by ID
|
|
279
|
+
- Hide database/storage implementation
|
|
280
|
+
- Return domain entities, not data structures
|
|
281
|
+
|
|
282
|
+
**Clojure Pattern:**
|
|
283
|
+
```clojure
|
|
284
|
+
;; Repository provides aggregate-level persistence
|
|
285
|
+
|
|
286
|
+
(defn get-account
|
|
287
|
+
"Returns Account entity by account-number, nil if not found."
|
|
288
|
+
[account-number]
|
|
289
|
+
(when-let [account-row (fetch-from-db account-number)]
|
|
290
|
+
(account-row->domain-entity account-row)))
|
|
291
|
+
|
|
292
|
+
(defn save-account
|
|
293
|
+
"Persists Account aggregate."
|
|
294
|
+
[account]
|
|
295
|
+
(let [account-row (domain-entity->account-row account)]
|
|
296
|
+
(persist-to-db account-row)))
|
|
297
|
+
|
|
298
|
+
;; Application Service orchestrates:
|
|
299
|
+
(defn transfer-money-use-case [transfer-number from-id to-id amount]
|
|
300
|
+
;; Get aggregates
|
|
301
|
+
(let [from-account (repository/get-account from-id)
|
|
302
|
+
to-account (repository/get-account to-id)
|
|
303
|
+
;; Execute domain logic (pure)
|
|
304
|
+
result (domain-service/transfer-money transfer-number
|
|
305
|
+
from-account
|
|
306
|
+
to-account
|
|
307
|
+
amount)]
|
|
308
|
+
;; Persist changes
|
|
309
|
+
(repository/commit-transfer-event result)
|
|
310
|
+
result))
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Repository vs DAO/Active Record:**
|
|
314
|
+
- Repository: Aggregate-oriented, domain-driven
|
|
315
|
+
- DAO: Table-oriented, database-driven
|
|
316
|
+
|
|
317
|
+
### Bounded Contexts
|
|
318
|
+
|
|
319
|
+
**Definition:** An explicit boundary within which a domain model applies. The same term can mean different things in different contexts.
|
|
320
|
+
|
|
321
|
+
**Purpose:**
|
|
322
|
+
- Manage complexity by dividing the domain
|
|
323
|
+
- Allow different models in different parts of the system
|
|
324
|
+
- Prevent model corruption from mixing concepts
|
|
325
|
+
|
|
326
|
+
**Key Insights:**
|
|
327
|
+
- Ubiquitous language is only ubiquitous within a context
|
|
328
|
+
- Same word can have different meanings in different contexts
|
|
329
|
+
- Make boundaries explicit and protect them
|
|
330
|
+
|
|
331
|
+
**Example:**
|
|
332
|
+
|
|
333
|
+
```
|
|
334
|
+
Bounded Context: Sales
|
|
335
|
+
- "Customer": Entity with sales history, credit limit
|
|
336
|
+
- "Product": Catalog item with pricing
|
|
337
|
+
|
|
338
|
+
Bounded Context: Shipping
|
|
339
|
+
- "Customer": Just name and shipping address
|
|
340
|
+
- "Product": Weight and dimensions for shipping
|
|
341
|
+
|
|
342
|
+
Bounded Context: Accounting
|
|
343
|
+
- "Customer": Billing entity with payment terms
|
|
344
|
+
- "Product": Revenue recognition rules
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Context Mapping:**
|
|
348
|
+
|
|
349
|
+
Relationships between bounded contexts:
|
|
350
|
+
|
|
351
|
+
1. **Shared Kernel**: Two contexts share a subset of the model
|
|
352
|
+
2. **Customer/Supplier**: Downstream context depends on upstream
|
|
353
|
+
3. **Conformist**: Downstream accepts upstream model as-is
|
|
354
|
+
4. **Anti-Corruption Layer**: Translate between contexts to protect model
|
|
355
|
+
5. **Separate Ways**: Contexts are completely independent
|
|
356
|
+
|
|
357
|
+
**Clojure Organization:**
|
|
358
|
+
```clojure
|
|
359
|
+
;; Each bounded context as a separate namespace or library
|
|
360
|
+
|
|
361
|
+
;; sales/
|
|
362
|
+
;; domain_model.clj
|
|
363
|
+
;; domain_services.clj
|
|
364
|
+
;; application_service.clj
|
|
365
|
+
;; repository.clj
|
|
366
|
+
|
|
367
|
+
;; shipping/
|
|
368
|
+
;; domain_model.clj
|
|
369
|
+
;; domain_services.clj
|
|
370
|
+
;; application_service.clj
|
|
371
|
+
;; repository.clj
|
|
372
|
+
|
|
373
|
+
;; Integration via anti-corruption layer:
|
|
374
|
+
(defn sales-customer->shipping-customer [sales-customer]
|
|
375
|
+
{:name (:name sales-customer)
|
|
376
|
+
:shipping-address (:default-address sales-customer)})
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Domain Events
|
|
380
|
+
|
|
381
|
+
**Definition:** Something important that happened in the domain that domain experts care about.
|
|
382
|
+
|
|
383
|
+
**Characteristics:**
|
|
384
|
+
- Named in past tense (OrderPlaced, PaymentProcessed, AccountDebited)
|
|
385
|
+
- Immutable facts
|
|
386
|
+
- Contain data about what happened
|
|
387
|
+
- Can trigger reactions in same or different bounded contexts
|
|
388
|
+
|
|
389
|
+
**Uses:**
|
|
390
|
+
1. **Within a bounded context**: Decouple domain logic
|
|
391
|
+
2. **Between bounded contexts**: Integration and eventual consistency
|
|
392
|
+
3. **Event Sourcing**: Store events as source of truth
|
|
393
|
+
|
|
394
|
+
**Clojure Pattern:**
|
|
395
|
+
```clojure
|
|
396
|
+
;; Domain events describe state changes
|
|
397
|
+
|
|
398
|
+
(s/def :debited-account/event #{:debited-account})
|
|
399
|
+
|
|
400
|
+
(s/def :account/debited-account
|
|
401
|
+
(s/keys :req-un [:debited-account/event
|
|
402
|
+
:account/number
|
|
403
|
+
:debited-account/amount-value]))
|
|
404
|
+
|
|
405
|
+
(defn debit-account [account debit]
|
|
406
|
+
;; Returns domain event describing the change
|
|
407
|
+
(if (valid-debit? account debit)
|
|
408
|
+
{:event :debited-account
|
|
409
|
+
:number (:number account)
|
|
410
|
+
:amount-value (-> debit :amount :value)}
|
|
411
|
+
(throw (ex-info "Can't debit account" {...}))))
|
|
412
|
+
|
|
413
|
+
;; Application service interprets events:
|
|
414
|
+
(defn handle-transfer [result]
|
|
415
|
+
(let [{:keys [debited-account credited-account posted-transfer]} result]
|
|
416
|
+
;; Persist events
|
|
417
|
+
(persist-event debited-account)
|
|
418
|
+
(persist-event credited-account)
|
|
419
|
+
(persist-event posted-transfer)
|
|
420
|
+
;; Trigger side effects
|
|
421
|
+
(send-notification (:number debited-account))
|
|
422
|
+
(update-balance-cache debited-account credited-account)))
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## Functional DDD Architecture
|
|
426
|
+
|
|
427
|
+
### Layered Architecture (Functional Style)
|
|
428
|
+
|
|
429
|
+
**Domain Layer (Functional Core):**
|
|
430
|
+
- Pure functions
|
|
431
|
+
- No IO, no side effects
|
|
432
|
+
- Specs for entities, value objects, aggregates
|
|
433
|
+
- Domain services as pure transformations
|
|
434
|
+
- Returns new values or domain events
|
|
435
|
+
|
|
436
|
+
**Application Layer (Imperative Shell):**
|
|
437
|
+
- Orchestrates use cases
|
|
438
|
+
- Fetches data via repositories
|
|
439
|
+
- Calls pure domain functions
|
|
440
|
+
- Interprets domain events
|
|
441
|
+
- Performs side effects
|
|
442
|
+
|
|
443
|
+
**Infrastructure Layer:**
|
|
444
|
+
- Repositories (persistence)
|
|
445
|
+
- External services (HTTP, message queues)
|
|
446
|
+
- Framework integrations
|
|
447
|
+
|
|
448
|
+
**Example Structure:**
|
|
449
|
+
```clojure
|
|
450
|
+
;; Domain Layer (pure)
|
|
451
|
+
(ns my-app.domain.model
|
|
452
|
+
(:require [clojure.spec.alpha :as s]))
|
|
453
|
+
|
|
454
|
+
(s/def ::entity ...)
|
|
455
|
+
(defn make-entity [...] ...)
|
|
456
|
+
(defn update-entity [entity change] ...)
|
|
457
|
+
|
|
458
|
+
;; Application Layer (orchestration + effects)
|
|
459
|
+
(ns my-app.application
|
|
460
|
+
(:require [my-app.domain.model :as model]
|
|
461
|
+
[my-app.infrastructure.repository :as repo]))
|
|
462
|
+
|
|
463
|
+
(defn use-case [inputs]
|
|
464
|
+
(let [entity (repo/get-entity (:id inputs))
|
|
465
|
+
updated (model/update-entity entity inputs)]
|
|
466
|
+
(repo/save-entity updated)
|
|
467
|
+
updated))
|
|
468
|
+
|
|
469
|
+
;; Infrastructure Layer (IO)
|
|
470
|
+
(ns my-app.infrastructure.repository)
|
|
471
|
+
|
|
472
|
+
(defn get-entity [id]
|
|
473
|
+
;; Database access
|
|
474
|
+
...)
|
|
475
|
+
|
|
476
|
+
(defn save-entity [entity]
|
|
477
|
+
;; Database write
|
|
478
|
+
...)
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Functional Core, Imperative Shell
|
|
482
|
+
|
|
483
|
+
**Principle:** Maximize pure functional code, minimize and isolate side effects.
|
|
484
|
+
|
|
485
|
+
**Pattern:**
|
|
486
|
+
1. **Shell** reads inputs (IO)
|
|
487
|
+
2. **Shell** calls pure **Core** with data
|
|
488
|
+
3. **Core** returns results (pure computation)
|
|
489
|
+
4. **Shell** performs effects based on results
|
|
490
|
+
|
|
491
|
+
```clojure
|
|
492
|
+
;; CORE: Pure domain logic
|
|
493
|
+
(defn calculate-order-total [order]
|
|
494
|
+
(reduce + (map :price (:items order))))
|
|
495
|
+
|
|
496
|
+
(defn apply-discount [total discount-rules customer]
|
|
497
|
+
;; Pure calculation
|
|
498
|
+
...)
|
|
499
|
+
|
|
500
|
+
;; SHELL: Application service with effects
|
|
501
|
+
(defn process-order [order-request]
|
|
502
|
+
;; Read (effect)
|
|
503
|
+
(let [customer (db/get-customer (:customer-id order-request))
|
|
504
|
+
products (db/get-products (:product-ids order-request))
|
|
505
|
+
|
|
506
|
+
;; Pure domain logic
|
|
507
|
+
order (domain/create-order customer products order-request)
|
|
508
|
+
total (domain/calculate-order-total order)
|
|
509
|
+
final-total (domain/apply-discount total @discount-rules customer)
|
|
510
|
+
|
|
511
|
+
;; Write (effects)
|
|
512
|
+
saved-order (db/save-order (assoc order :total final-total))]
|
|
513
|
+
|
|
514
|
+
;; More effects
|
|
515
|
+
(email/send-confirmation customer saved-order)
|
|
516
|
+
(analytics/track-order saved-order)
|
|
517
|
+
|
|
518
|
+
saved-order))
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
## Practical Patterns from Clojure DDD
|
|
522
|
+
|
|
523
|
+
### Using Specs for Invariants
|
|
524
|
+
|
|
525
|
+
```clojure
|
|
526
|
+
;; Spec defines valid states
|
|
527
|
+
(s/def ::transfer
|
|
528
|
+
(s/and
|
|
529
|
+
(s/keys :req-un [::id ::number ::debit ::credit ::creation-date])
|
|
530
|
+
;; Invariants as predicates:
|
|
531
|
+
(fn [{:keys [debit credit]}]
|
|
532
|
+
(and
|
|
533
|
+
;; Same amount debited and credited
|
|
534
|
+
(= (:amount debit) (:amount credit))
|
|
535
|
+
;; Different accounts
|
|
536
|
+
(not= (:number debit) (:number credit))))))
|
|
537
|
+
|
|
538
|
+
;; Constructor validates on creation
|
|
539
|
+
(defn make-transfer [transfer-number debit credit]
|
|
540
|
+
(s/assert ::transfer
|
|
541
|
+
{:id (random-uuid)
|
|
542
|
+
:number transfer-number
|
|
543
|
+
:debit debit
|
|
544
|
+
:credit credit
|
|
545
|
+
:creation-date (java.util.Date.)}))
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### Event-Driven State Changes
|
|
549
|
+
|
|
550
|
+
Instead of mutating entities, return events describing changes:
|
|
551
|
+
|
|
552
|
+
```clojure
|
|
553
|
+
;; Instead of: (set! account.balance new-balance)
|
|
554
|
+
|
|
555
|
+
;; Return event:
|
|
556
|
+
(defn debit-account [account debit]
|
|
557
|
+
(if (can-debit? account debit)
|
|
558
|
+
{:event :debited-account
|
|
559
|
+
:account-number (:number account)
|
|
560
|
+
:amount (:value debit)
|
|
561
|
+
:timestamp (now)}
|
|
562
|
+
(throw (ex-info "Cannot debit" {...}))))
|
|
563
|
+
|
|
564
|
+
;; Repository interprets event:
|
|
565
|
+
(defn commit-debit-event [event]
|
|
566
|
+
(swap! state update-in
|
|
567
|
+
[:accounts (:account-number event)]
|
|
568
|
+
apply-debit
|
|
569
|
+
(:amount event)))
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### Eventual Consistency Trade-offs
|
|
573
|
+
|
|
574
|
+
From the Clojure example:
|
|
575
|
+
```clojure
|
|
576
|
+
;; With eventual consistency:
|
|
577
|
+
;; - Can process 2000 concurrent transfers
|
|
578
|
+
;; - Never double-spend (total money is conserved)
|
|
579
|
+
;; - BUT: Account can go temporarily negative
|
|
580
|
+
|
|
581
|
+
;; Business decision: Is this acceptable?
|
|
582
|
+
;; - Maybe: charge overdraft fee
|
|
583
|
+
;; - Maybe: customer can cover temporarily
|
|
584
|
+
;; - Trade-off: massive scalability for eventual consistency
|
|
585
|
+
|
|
586
|
+
;; If not acceptable: Use strong consistency (transactions, locks)
|
|
587
|
+
;; Trade-off: Lower scalability but immediate consistency
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
## Anti-Patterns to Avoid
|
|
591
|
+
|
|
592
|
+
### Anemic Domain Model
|
|
593
|
+
|
|
594
|
+
**Problem:** Entities are just data holders; all logic in services.
|
|
595
|
+
|
|
596
|
+
```clojure
|
|
597
|
+
;; ANEMIC (Bad):
|
|
598
|
+
(s/def ::account (s/keys :req-un [::number ::balance]))
|
|
599
|
+
|
|
600
|
+
(defn debit-account [account amount]
|
|
601
|
+
(update account :balance - amount)) ; No validation!
|
|
602
|
+
|
|
603
|
+
;; Service does all validation:
|
|
604
|
+
(defn debit-with-validation [account amount]
|
|
605
|
+
(if (>= (:balance account) amount)
|
|
606
|
+
(debit-account account amount)
|
|
607
|
+
(throw ...)))
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
**Better:** Put invariants in domain model
|
|
611
|
+
```clojure
|
|
612
|
+
;; Domain model validates:
|
|
613
|
+
(s/def ::account
|
|
614
|
+
(s/and
|
|
615
|
+
(s/keys :req-un [::number ::balance])
|
|
616
|
+
#(>= (:balance %) 0))) ; Invariant: balance never negative
|
|
617
|
+
|
|
618
|
+
(defn debit-account [account amount]
|
|
619
|
+
(let [new-account (update account :balance - amount)]
|
|
620
|
+
(s/assert ::account new-account))) ; Validates invariant
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### God Aggregates
|
|
624
|
+
|
|
625
|
+
**Problem:** Massive aggregates that do everything.
|
|
626
|
+
|
|
627
|
+
**Better:** Keep aggregates small and focused.
|
|
628
|
+
|
|
629
|
+
### Missing Bounded Contexts
|
|
630
|
+
|
|
631
|
+
**Problem:** One model trying to serve all use cases.
|
|
632
|
+
|
|
633
|
+
**Better:** Separate models for separate contexts.
|
|
634
|
+
|
|
635
|
+
## Checklist for DDD Implementation
|
|
636
|
+
|
|
637
|
+
- [ ] **Ubiquitous Language**: Same terms in code and conversations
|
|
638
|
+
- [ ] **Entities have clear identity**: Can track over time
|
|
639
|
+
- [ ] **Value objects are immutable**: Defined by attributes
|
|
640
|
+
- [ ] **Aggregates enforce invariants**: Consistency boundaries are clear
|
|
641
|
+
- [ ] **Operations through aggregate roots**: No direct access to internals
|
|
642
|
+
- [ ] **Domain services for multi-aggregate operations**: Pure functions
|
|
643
|
+
- [ ] **Repositories at aggregate level**: Load/save whole aggregates
|
|
644
|
+
- [ ] **Application services orchestrate**: Thin layer calling domain
|
|
645
|
+
- [ ] **Bounded contexts are explicit**: Clear boundaries and integration
|
|
646
|
+
- [ ] **Domain events capture important happenings**: Past tense, immutable
|
|
647
|
+
|
|
648
|
+
## Key Takeaways
|
|
649
|
+
|
|
650
|
+
1. **Start with language**: Listen to domain experts, extract ubiquitous language
|
|
651
|
+
2. **Entities vs Value Objects**: Identity vs attributes
|
|
652
|
+
3. **Aggregates are consistency boundaries**: Keep them small
|
|
653
|
+
4. **Domain logic is pure**: No side effects in domain model/services
|
|
654
|
+
5. **Repository abstracts persistence**: Aggregate-oriented
|
|
655
|
+
6. **Bounded contexts divide complexity**: Different models for different contexts
|
|
656
|
+
7. **Domain events decouple**: Integration and eventual consistency
|
|
657
|
+
8. **Functional core, imperative shell**: Maximize purity, isolate effects
|
|
658
|
+
|
|
659
|
+
Always ask:
|
|
660
|
+
- What does the domain expert call this?
|
|
661
|
+
- Does this have identity or just value?
|
|
662
|
+
- What are the invariants?
|
|
663
|
+
- What's the consistency boundary?
|
|
664
|
+
- Which context are we in?
|