@dynokostya/just-works 1.0.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.
- package/.claude/agents/csharp-code-writer.md +32 -0
- package/.claude/agents/diagrammer.md +49 -0
- package/.claude/agents/frontend-code-writer.md +36 -0
- package/.claude/agents/prompt-writer.md +38 -0
- package/.claude/agents/python-code-writer.md +32 -0
- package/.claude/agents/swift-code-writer.md +32 -0
- package/.claude/agents/typescript-code-writer.md +32 -0
- package/.claude/commands/git-sync.md +96 -0
- package/.claude/commands/project-docs.md +287 -0
- package/.claude/settings.json +112 -0
- package/.claude/settings.json.default +15 -0
- package/.claude/skills/csharp-coding/SKILL.md +368 -0
- package/.claude/skills/ddd-architecture-python/SKILL.md +288 -0
- package/.claude/skills/feature-driven-architecture-python/SKILL.md +302 -0
- package/.claude/skills/gemini-3-prompting/SKILL.md +483 -0
- package/.claude/skills/gpt-5-2-prompting/SKILL.md +295 -0
- package/.claude/skills/opus-4-6-prompting/SKILL.md +315 -0
- package/.claude/skills/plantuml-diagramming/SKILL.md +758 -0
- package/.claude/skills/python-coding/SKILL.md +293 -0
- package/.claude/skills/react-coding/SKILL.md +264 -0
- package/.claude/skills/rest-api/SKILL.md +421 -0
- package/.claude/skills/shadcn-ui-coding/SKILL.md +454 -0
- package/.claude/skills/swift-coding/SKILL.md +401 -0
- package/.claude/skills/tailwind-css-coding/SKILL.md +268 -0
- package/.claude/skills/typescript-coding/SKILL.md +464 -0
- package/.claude/statusline-command.sh +34 -0
- package/.codex/prompts/plan-reviewer.md +162 -0
- package/.codex/prompts/project-docs.md +287 -0
- package/.codex/skills/ddd-architecture-python/SKILL.md +288 -0
- package/.codex/skills/feature-driven-architecture-python/SKILL.md +302 -0
- package/.codex/skills/gemini-3-prompting/SKILL.md +483 -0
- package/.codex/skills/gpt-5-2-prompting/SKILL.md +295 -0
- package/.codex/skills/opus-4-6-prompting/SKILL.md +315 -0
- package/.codex/skills/plantuml-diagramming/SKILL.md +758 -0
- package/.codex/skills/python-coding/SKILL.md +293 -0
- package/.codex/skills/react-coding/SKILL.md +264 -0
- package/.codex/skills/rest-api/SKILL.md +421 -0
- package/.codex/skills/shadcn-ui-coding/SKILL.md +454 -0
- package/.codex/skills/tailwind-css-coding/SKILL.md +268 -0
- package/.codex/skills/typescript-coding/SKILL.md +464 -0
- package/AGENTS.md +57 -0
- package/CLAUDE.md +98 -0
- package/LICENSE +201 -0
- package/README.md +114 -0
- package/bin/cli.mjs +291 -0
- package/package.json +39 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# Project Documentation
|
|
2
|
+
|
|
3
|
+
Generate or update project documentation in `docs/`. Produces three files:
|
|
4
|
+
|
|
5
|
+
- `docs/mission.md` — What the project is and who it's for
|
|
6
|
+
- `docs/tech-stack.md` — Inventory of languages, frameworks, tools, infrastructure
|
|
7
|
+
- `docs/architecture.md` — How the system is structured and how parts connect
|
|
8
|
+
|
|
9
|
+
## Phase 1: Detect State
|
|
10
|
+
|
|
11
|
+
Classify each file independently:
|
|
12
|
+
|
|
13
|
+
| File | Exists & non-empty | Status |
|
|
14
|
+
|------|-------------------|--------|
|
|
15
|
+
| `docs/mission.md` | yes | **update** |
|
|
16
|
+
| `docs/mission.md` | no | **create** |
|
|
17
|
+
| `docs/tech-stack.md` | yes | **update** |
|
|
18
|
+
| `docs/tech-stack.md` | no | **create** |
|
|
19
|
+
| `docs/architecture.md` | yes | **update** |
|
|
20
|
+
| `docs/architecture.md` | no | **create** |
|
|
21
|
+
|
|
22
|
+
A file with only whitespace or markdown headers with no content counts as **create**, not update.
|
|
23
|
+
|
|
24
|
+
**Git context gathering.** If `.git` exists, run these two commands via Bash and include the output as background context for all Explore agents in Phase 2:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
git log --oneline -30
|
|
28
|
+
git shortlog -s -n --no-merges
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If the repo has fewer than 5 commits, note this — it signals an early-stage project where exploration will find less and user questions become more important.
|
|
32
|
+
|
|
33
|
+
Announce the per-file status table and commit count to the user before continuing.
|
|
34
|
+
|
|
35
|
+
## Phase 2: Explore
|
|
36
|
+
|
|
37
|
+
Launch three parallel Explore agents using the Task tool (`subagent_type: "Explore"`). All three are independent — launch them in a single message, never sequentially. Specify `thoroughness: "very thorough"` in each prompt.
|
|
38
|
+
|
|
39
|
+
Every agent prompt must include:
|
|
40
|
+
- The git context gathered in Phase 1 (last 30 commit subjects + contributor summary)
|
|
41
|
+
- This instruction: **"For every finding, include the source file path and line number (e.g., `src/main.py:42`). Findings without a file reference will be discarded."**
|
|
42
|
+
- This ignore directive: **"Skip these directories entirely: `node_modules/`, `.venv/`, `venv/`, `__pycache__/`, `dist/`, `build/`, `.git/`, `.next/`, `.nuxt/`, `target/`, `vendor/` (unless vendor is committed Go code)."**
|
|
43
|
+
|
|
44
|
+
### Agent 1: Architecture
|
|
45
|
+
|
|
46
|
+
> Explore the codebase very thoroughly for architectural information. For every finding, include the source file path and line number (e.g., `src/main.py:42`). Findings without a file reference will be discarded. Skip: node_modules/, .venv/, venv/, __pycache__/, dist/, build/, .git/, .next/, .nuxt/, target/, vendor/.
|
|
47
|
+
>
|
|
48
|
+
> Look for:
|
|
49
|
+
> - Top-level directory structure and what each directory contains
|
|
50
|
+
> - Module/package boundaries and dependency direction between them
|
|
51
|
+
> - Entry points: main scripts, CLI commands, API servers, workers, scheduled jobs
|
|
52
|
+
> - Design patterns: MVC, hexagonal, event-driven, repository pattern, etc.
|
|
53
|
+
> - Data flow: how a request or input travels from entry point to response
|
|
54
|
+
> - Configuration management: env files, config modules, feature flags
|
|
55
|
+
> - Test organization relative to source code
|
|
56
|
+
> - Whether this is a monorepo (multiple package manifests, separate apps in subdirectories)
|
|
57
|
+
|
|
58
|
+
### Agent 2: Tech Stack
|
|
59
|
+
|
|
60
|
+
> Explore the codebase very thoroughly for technology inventory. For every finding, include the source file path and line number (e.g., `pyproject.toml:3`). Findings without a file reference will be discarded. Skip: node_modules/, .venv/, venv/, __pycache__/, dist/, build/, .git/, .next/, .nuxt/, target/, vendor/.
|
|
61
|
+
>
|
|
62
|
+
> Look for:
|
|
63
|
+
> - Programming languages and their versions (configs, CI, runtime files, shebangs)
|
|
64
|
+
> - Package manifests: pyproject.toml, package.json, Cargo.toml, go.mod, Gemfile, etc.
|
|
65
|
+
> - Frameworks: web, ORM, task queues, testing, CLI
|
|
66
|
+
> - Databases and storage: connection strings, migrations, docker-compose services
|
|
67
|
+
> - Infrastructure: Dockerfiles, terraform, k8s manifests, CI/CD pipelines, deployment configs
|
|
68
|
+
> - Dev tools: linters, formatters, type checkers, test runners, pre-commit hooks
|
|
69
|
+
> - External services: API client imports, SDK usage, webhook handlers, third-party integrations
|
|
70
|
+
|
|
71
|
+
### Agent 3: Mission & Purpose
|
|
72
|
+
|
|
73
|
+
> Explore the codebase very thoroughly for project purpose and audience. For every finding, include the source file path and line number (e.g., `README.md:1`). Findings without a file reference will be discarded. Skip: node_modules/, .venv/, venv/, __pycache__/, dist/, build/, .git/, .next/, .nuxt/, target/, vendor/.
|
|
74
|
+
>
|
|
75
|
+
> Look for:
|
|
76
|
+
> - README.md and any ABOUT or CONTRIBUTING files
|
|
77
|
+
> - Package/project descriptions in manifests (pyproject.toml description, package.json description)
|
|
78
|
+
> - User-facing text: landing pages, onboarding flows, help text, CLI descriptions
|
|
79
|
+
> - API descriptions, OpenAPI specs, GraphQL schema descriptions
|
|
80
|
+
> - Comments or docstrings describing project purpose
|
|
81
|
+
> - License and contribution guidelines
|
|
82
|
+
> - Any marketing copy, about pages, or FAQ content
|
|
83
|
+
|
|
84
|
+
## Phase 3: Synthesize & Confirm
|
|
85
|
+
|
|
86
|
+
After all three agents return, consolidate their findings into a structured summary organized by document. **Discard any finding that lacks a file path reference** — this enforces the "no invention" rule.
|
|
87
|
+
|
|
88
|
+
### For files in **create** status
|
|
89
|
+
|
|
90
|
+
Present the summary to the user. Then identify genuine gaps — things the code did not clearly answer.
|
|
91
|
+
|
|
92
|
+
**Gap detection heuristics** — ask only when the trigger condition is met:
|
|
93
|
+
|
|
94
|
+
Mission gaps:
|
|
95
|
+
- **What problem does this solve?** → Trigger: no README exists, or README has no description beyond project name/install instructions
|
|
96
|
+
- **Who is the target user?** → Trigger: no user-facing text found (no CLI help, no UI copy, no API descriptions)
|
|
97
|
+
- **What differentiates this?** → Trigger: README does not mention alternatives or positioning
|
|
98
|
+
|
|
99
|
+
Tech-stack gaps:
|
|
100
|
+
- **Ambiguous primary tool** → Trigger: multiple tools serving the same role found (e.g., two ORMs, two test frameworks)
|
|
101
|
+
- **Deployment target** → Trigger: no Dockerfile, no CI/CD config, no infra files found
|
|
102
|
+
- **External services** → Trigger: code references services (database URLs, API keys) but no config or docker-compose defines them
|
|
103
|
+
|
|
104
|
+
Architecture gaps:
|
|
105
|
+
- **Intended vs actual boundaries** → Trigger: import cycles detected or modules with unclear ownership
|
|
106
|
+
- **Missing components** → Trigger: code references modules/packages that don't exist yet
|
|
107
|
+
|
|
108
|
+
Rules for questions:
|
|
109
|
+
- Do NOT ask about things the code clearly answers. Every question must address a genuine gap where the trigger condition was met.
|
|
110
|
+
- When exploration found relevant evidence, provide 2-4 concrete options derived from findings.
|
|
111
|
+
- When exploration found nothing relevant (common for "target user" or "deployment target" in early projects), ask as a free-text question without forced options.
|
|
112
|
+
- The AskUserQuestion tool automatically provides an "Other" free-text option — do not add one manually.
|
|
113
|
+
- If no trigger conditions are met, skip questions entirely. It is acceptable to have zero questions.
|
|
114
|
+
|
|
115
|
+
After gaps are resolved, present the planned content for each **create** file and ask the user to approve before writing.
|
|
116
|
+
|
|
117
|
+
### For files in **update** status
|
|
118
|
+
|
|
119
|
+
Read the existing docs. Compare each against the exploration findings.
|
|
120
|
+
|
|
121
|
+
Present a structured change summary:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
Changes detected:
|
|
125
|
+
|
|
126
|
+
architecture.md:
|
|
127
|
+
+ New module `workers/` found (src/workers/__init__.py:1), not documented
|
|
128
|
+
~ Description of `api/` outdated — now uses FastAPI (pyproject.toml:15) instead of Flask
|
|
129
|
+
- Module `legacy/` removed from codebase but still in docs
|
|
130
|
+
|
|
131
|
+
tech-stack.md:
|
|
132
|
+
+ Redis added (docker-compose.yml:23)
|
|
133
|
+
- Removed: celery no longer in dependencies
|
|
134
|
+
|
|
135
|
+
mission.md:
|
|
136
|
+
No changes detected
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Every `+` and `~` line must cite the source file. Changes without evidence are not presented.
|
|
140
|
+
|
|
141
|
+
Ask the user to approve, modify, or reject changes per file. Only write files the user approves.
|
|
142
|
+
|
|
143
|
+
If exploration reveals something contradicting existing docs and the correct answer is ambiguous, ask the user before deciding.
|
|
144
|
+
|
|
145
|
+
### Monorepo handling
|
|
146
|
+
|
|
147
|
+
If Agent 1 identified multiple separate applications (e.g., `frontend/`, `backend/`, `services/auth/`), restructure the output:
|
|
148
|
+
|
|
149
|
+
- `tech-stack.md` — group items under subheadings per service/app instead of a flat list
|
|
150
|
+
- `architecture.md` — add a "Services" section before "Module Boundaries" describing each top-level app and how they communicate
|
|
151
|
+
- `mission.md` — no change (mission is project-wide)
|
|
152
|
+
|
|
153
|
+
## Phase 4: Write
|
|
154
|
+
|
|
155
|
+
Create `docs/` directory if it does not exist. Write each approved file using these structures.
|
|
156
|
+
|
|
157
|
+
### docs/mission.md
|
|
158
|
+
|
|
159
|
+
```markdown
|
|
160
|
+
# Mission
|
|
161
|
+
|
|
162
|
+
## What
|
|
163
|
+
|
|
164
|
+
[1-2 sentences: what the project does, stated as fact]
|
|
165
|
+
|
|
166
|
+
## Who
|
|
167
|
+
|
|
168
|
+
[1-2 sentences: target users or audience]
|
|
169
|
+
|
|
170
|
+
## Why
|
|
171
|
+
|
|
172
|
+
[1-2 sentences: core problem being solved, value delivered]
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
*Generated: YYYY-MM-DD | Commit: abc1234*
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
10-15 lines max (excluding footer). No aspirational language. No marketing fluff.
|
|
179
|
+
|
|
180
|
+
### docs/tech-stack.md
|
|
181
|
+
|
|
182
|
+
```markdown
|
|
183
|
+
# Tech Stack
|
|
184
|
+
|
|
185
|
+
## Languages
|
|
186
|
+
|
|
187
|
+
- [Language] [version] — [source: pyproject.toml / Dockerfile / .python-version]
|
|
188
|
+
|
|
189
|
+
## Frameworks
|
|
190
|
+
|
|
191
|
+
- [Framework] — [one-line role, e.g. "web server", "ORM", "task queue"]
|
|
192
|
+
|
|
193
|
+
## Storage
|
|
194
|
+
|
|
195
|
+
- [Database/cache/queue] — [one-line role]
|
|
196
|
+
|
|
197
|
+
## Infrastructure
|
|
198
|
+
|
|
199
|
+
- [Tool] — [one-line role]
|
|
200
|
+
|
|
201
|
+
## Dev Tools
|
|
202
|
+
|
|
203
|
+
- [Tool] — [one-line role]
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
*Generated: YYYY-MM-DD | Commit: abc1234*
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Flat list. One line per item. No prose paragraphs. Only include sections that have at least one entry. Omit empty sections entirely.
|
|
210
|
+
|
|
211
|
+
For monorepos, replace flat sections with grouped subheadings:
|
|
212
|
+
```markdown
|
|
213
|
+
## Frameworks
|
|
214
|
+
|
|
215
|
+
### backend/
|
|
216
|
+
- FastAPI — web server
|
|
217
|
+
- SQLAlchemy — ORM
|
|
218
|
+
|
|
219
|
+
### frontend/
|
|
220
|
+
- Next.js — React framework
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### docs/architecture.md
|
|
224
|
+
|
|
225
|
+
```markdown
|
|
226
|
+
# Architecture
|
|
227
|
+
|
|
228
|
+
## Structure
|
|
229
|
+
|
|
230
|
+
[Brief description of top-level project organization. Reference actual directory names.]
|
|
231
|
+
|
|
232
|
+
## Module Boundaries
|
|
233
|
+
|
|
234
|
+
[Which modules exist, what each owns, dependency direction between them.]
|
|
235
|
+
|
|
236
|
+
## Data Flow
|
|
237
|
+
|
|
238
|
+
[How a typical request/input travels through the system, from entry point to response/output.]
|
|
239
|
+
|
|
240
|
+
## Key Patterns
|
|
241
|
+
|
|
242
|
+
[Design patterns in use: name, where applied, one-line rationale.]
|
|
243
|
+
|
|
244
|
+
## Entry Points
|
|
245
|
+
|
|
246
|
+
- [Entry point] — [what it starts/serves]
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
*Generated: YYYY-MM-DD | Commit: abc1234*
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Reference tech-stack items by name. Do not duplicate their descriptions.
|
|
253
|
+
|
|
254
|
+
### Footer values
|
|
255
|
+
|
|
256
|
+
- **Date**: current date in YYYY-MM-DD format
|
|
257
|
+
- **Commit**: short hash from `git rev-parse --short HEAD` (if `.git` exists). If no git repo, omit the commit portion and use only the date.
|
|
258
|
+
|
|
259
|
+
## Phase 5: Verify
|
|
260
|
+
|
|
261
|
+
After writing all files, perform a verification pass. For each generated document:
|
|
262
|
+
|
|
263
|
+
1. Read the file back.
|
|
264
|
+
2. For every factual claim (tool name, framework, directory name, pattern), confirm it traces to a specific file found during exploration.
|
|
265
|
+
3. If a claim cannot be traced — remove it and note the removal to the user.
|
|
266
|
+
|
|
267
|
+
Report verification results:
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
Verification:
|
|
271
|
+
mission.md — 3/3 claims verified ✓
|
|
272
|
+
tech-stack.md — 11/12 claims verified, removed: "GraphQL" (no schema or dependency found)
|
|
273
|
+
architecture.md — 8/8 claims verified ✓
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
If all claims verify, report clean and finish. If removals were made, show what was removed and why.
|
|
277
|
+
|
|
278
|
+
## Rules
|
|
279
|
+
|
|
280
|
+
These apply to all phases:
|
|
281
|
+
|
|
282
|
+
- **No invention.** Every statement must trace to a file path found by exploration or to user confirmation. Findings without source references are discarded.
|
|
283
|
+
- **No aspiration.** Document what exists, not what is planned. No "we plan to", "future work", "upcoming".
|
|
284
|
+
- **No padding.** If a section has nothing to say, omit it. Do not write filler content.
|
|
285
|
+
- **Terse language.** These docs are reference material for engineers. Factual, direct, no marketing tone.
|
|
286
|
+
- **Respect the templates.** Do not add sections beyond what the templates define unless the user explicitly requests them.
|
|
287
|
+
- **Evidence required.** Explore agents must cite file paths. The synthesis phase must cite file paths. The change summary must cite file paths. Uncited claims are removed during verification.
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ddd-architecture-python
|
|
3
|
+
description: Apply when implementing Domain-Driven Design patterns in Python (.py) files. Covers tactical patterns (entities, value objects, aggregates, domain events, repositories), layered architecture with dependency inversion, persistence strategies, validation boundaries, and common DDD anti-patterns. Best suited for projects with complex business rules spanning multiple entities.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Domain-Driven Design in Python
|
|
7
|
+
|
|
8
|
+
Match the project's existing domain model conventions. When uncertain, read 2-3 existing aggregate or entity modules to infer the local style. Check for existing base classes, event infrastructure, and repository patterns before introducing new ones. These defaults apply only when the project has no established convention.
|
|
9
|
+
|
|
10
|
+
## Never rules
|
|
11
|
+
|
|
12
|
+
These are unconditional. They prevent structural defects regardless of project style.
|
|
13
|
+
|
|
14
|
+
- **Never put business logic in service layers while domain models are empty data bags.** This is the anemic domain model. If all methods live in services and entities are just data carriers, you have Transaction Scripts with extra mapping cost. Move behavior that enforces invariants into the entity or aggregate that owns the state.
|
|
15
|
+
- **Never create repositories for anything other than aggregate roots.** Repositories exist per aggregate root, not per entity. Accessing child entities bypassing the aggregate root breaks consistency boundaries. `OrderLineRepository` is always wrong if `OrderLine` belongs to an `Order` aggregate.
|
|
16
|
+
- **Never use `@dataclass(frozen=True)` for entities.** Frozen dataclasses enforce structural equality (compare all fields). Entities have identity -- two `User` objects with the same `id` are the same user even if `email` changed. Use `@dataclass(eq=False, slots=True)` and implement identity-based `__eq__` and `__hash__` on the id field.
|
|
17
|
+
- **Never use `unsafe_hash=True` on mutable dataclasses.** It makes mutable objects hashable, causing subtle bugs when attributes change after insertion into sets or dict keys. Use frozen for value objects, custom hash for entities.
|
|
18
|
+
- **Never let domain models import from infrastructure.** The dependency arrow points inward: infrastructure -> application -> domain. Domain models must not import SQLAlchemy, Pydantic, httpx, or any external framework.
|
|
19
|
+
- **Never duplicate validation between API layer and domain layer.** Pydantic validates input shape at the boundary (type coercion, required fields). Domain validates business invariants (order total can't be negative, user can't have more than 5 active subscriptions). These are different concerns.
|
|
20
|
+
- **Never apply tactical DDD patterns to CRUD-only modules.** If a bounded context has no business invariants beyond "save and retrieve," use plain service functions or direct ORM operations. Strategic DDD (bounded contexts, ubiquitous language) is almost always valuable; tactical DDD is conditional.
|
|
21
|
+
|
|
22
|
+
## Tactical patterns
|
|
23
|
+
|
|
24
|
+
| Pattern | Python Implementation | Use When | Skip When |
|
|
25
|
+
|---------|----------------------|----------|-----------|
|
|
26
|
+
| **Value Object** | `@dataclass(frozen=True, slots=True)` | Equality by value (Money, Email, DateRange) | Simple strings/ints with no validation |
|
|
27
|
+
| **Entity** | `@dataclass(eq=False, slots=True)` + custom `__eq__`/`__hash__` on id | Objects with lifecycle and identity | Lookup tables, config records |
|
|
28
|
+
| **Aggregate Root** | Entity + `_events: list[Event]` + invariant methods | Multi-entity consistency boundaries | Single-entity modules |
|
|
29
|
+
| **Domain Event** | `@dataclass(frozen=True)` inheriting from `Event` base | Side effects: notifications, indexing, cross-context sync | Simple CRUD with no cross-context effects |
|
|
30
|
+
| **Repository** | Protocol in domain, implementation in infrastructure | Aggregate root persistence abstraction | Simple modules -- use ORM directly |
|
|
31
|
+
| **Domain Service** | Plain function or class in domain layer | Logic spanning multiple aggregates | Logic that belongs on a single entity |
|
|
32
|
+
| **Application Service** | Orchestrates repositories, domain objects, UoW | Use case coordination | Don't mix with domain logic |
|
|
33
|
+
| **Factory** | `@classmethod` on entity or aggregate | Complex construction with invariants | Simple `__init__` suffices |
|
|
34
|
+
|
|
35
|
+
### Value object
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class Money:
|
|
40
|
+
amount: Decimal
|
|
41
|
+
currency: str
|
|
42
|
+
|
|
43
|
+
def __post_init__(self) -> None:
|
|
44
|
+
if self.amount < 0:
|
|
45
|
+
raise ValueError("Amount cannot be negative")
|
|
46
|
+
if len(self.currency) != 3:
|
|
47
|
+
raise ValueError("Currency must be ISO 4217 code")
|
|
48
|
+
|
|
49
|
+
def add(self, other: Money) -> Money:
|
|
50
|
+
if self.currency != other.currency:
|
|
51
|
+
raise ValueError(f"Cannot add {self.currency} to {other.currency}")
|
|
52
|
+
return Money(amount=self.amount + other.amount, currency=self.currency)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Entity with identity equality
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
@dataclass(eq=False, slots=True)
|
|
59
|
+
class Order:
|
|
60
|
+
id: int
|
|
61
|
+
customer_id: int
|
|
62
|
+
lines: list[OrderLine]
|
|
63
|
+
status: OrderStatus
|
|
64
|
+
_events: list[Event] = field(default_factory=list, repr=False)
|
|
65
|
+
|
|
66
|
+
def __eq__(self, other: object) -> bool:
|
|
67
|
+
if not isinstance(other, Order):
|
|
68
|
+
return NotImplemented
|
|
69
|
+
return self.id == other.id
|
|
70
|
+
|
|
71
|
+
def __hash__(self) -> int:
|
|
72
|
+
return hash(self.id)
|
|
73
|
+
|
|
74
|
+
def add_line(self, sku: str, qty: int, price: Money) -> None:
|
|
75
|
+
if self.status != OrderStatus.DRAFT:
|
|
76
|
+
raise OrderFinalizedError(self.id)
|
|
77
|
+
line = OrderLine(sku=sku, qty=qty, price=price)
|
|
78
|
+
self.lines.append(line)
|
|
79
|
+
|
|
80
|
+
def confirm(self) -> None:
|
|
81
|
+
if not self.lines:
|
|
82
|
+
raise EmptyOrderError(self.id)
|
|
83
|
+
self.status = OrderStatus.CONFIRMED
|
|
84
|
+
self._events.append(OrderConfirmed(order_id=self.id))
|
|
85
|
+
|
|
86
|
+
def collect_events(self) -> list[Event]:
|
|
87
|
+
events = self._events[:]
|
|
88
|
+
self._events.clear()
|
|
89
|
+
return events
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Wrong -- frozen dataclass for an entity:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
# WRONG: frozen enforces structural equality, entities have identity
|
|
96
|
+
@dataclass(frozen=True, slots=True)
|
|
97
|
+
class Order:
|
|
98
|
+
id: int
|
|
99
|
+
customer_id: int
|
|
100
|
+
status: OrderStatus # can't mutate status transitions
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Persistence strategy
|
|
104
|
+
|
|
105
|
+
Three approaches, ordered by coupling:
|
|
106
|
+
|
|
107
|
+
| Strategy | Tradeoff | Use When |
|
|
108
|
+
|----------|----------|----------|
|
|
109
|
+
| **Domain models = ORM models** | Coupling to SQLAlchemy, but zero mapping | <3 aggregates, team <3, domain is simple |
|
|
110
|
+
| **Imperative mapping** (`map_imperatively`) | Clean separation, moderate setup | Business logic complex enough to test without DB |
|
|
111
|
+
| **Separate models + manual mapping** | Full isolation, high boilerplate | Domain and persistence schemas diverge significantly |
|
|
112
|
+
|
|
113
|
+
Start with Strategy A. Move to Strategy B when you need to unit-test domain logic without touching the database. Move to Strategy C only when the persistence schema genuinely differs from the domain model.
|
|
114
|
+
|
|
115
|
+
### Imperative mapping
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
# domain/model.py -- pure Python, no SQLAlchemy imports
|
|
119
|
+
@dataclass(eq=False, slots=True)
|
|
120
|
+
class Product:
|
|
121
|
+
id: int
|
|
122
|
+
sku: str
|
|
123
|
+
batches: list[Batch]
|
|
124
|
+
|
|
125
|
+
# infrastructure/orm.py -- SQLAlchemy mapping, called once at startup
|
|
126
|
+
from sqlalchemy import Table, Column, Integer, String
|
|
127
|
+
from sqlalchemy.orm import registry, relationship
|
|
128
|
+
|
|
129
|
+
mapper_registry = registry()
|
|
130
|
+
|
|
131
|
+
product_table = Table(
|
|
132
|
+
"products",
|
|
133
|
+
mapper_registry.metadata,
|
|
134
|
+
Column("id", Integer, primary_key=True),
|
|
135
|
+
Column("sku", String(255)),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def start_mappers() -> None:
|
|
139
|
+
mapper_registry.map_imperatively(Product, product_table, properties={
|
|
140
|
+
"batches": relationship(Batch),
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Dependency inversion
|
|
145
|
+
|
|
146
|
+
| Mechanism | Use When |
|
|
147
|
+
|-----------|----------|
|
|
148
|
+
| **Protocol** | Domain ports consumed by application/infrastructure. No inheritance required. |
|
|
149
|
+
| **ABC** | Infrastructure base classes where implementations share common behavior |
|
|
150
|
+
| **Constructor args** | Default for everything. Explicit, testable, no framework. |
|
|
151
|
+
| **FastAPI Depends** | Request-scoped injection in web handlers |
|
|
152
|
+
|
|
153
|
+
Protocol for domain ports, constructor injection for wiring:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# domain/ports.py
|
|
157
|
+
from typing import Protocol
|
|
158
|
+
|
|
159
|
+
class OrderRepository(Protocol):
|
|
160
|
+
async def get(self, order_id: int) -> Order: ...
|
|
161
|
+
async def save(self, order: Order) -> None: ...
|
|
162
|
+
|
|
163
|
+
# application/services.py
|
|
164
|
+
class ConfirmOrderHandler:
|
|
165
|
+
def __init__(self, repo: OrderRepository, bus: MessageBus) -> None:
|
|
166
|
+
self._repo = repo
|
|
167
|
+
self._bus = bus
|
|
168
|
+
|
|
169
|
+
async def handle(self, command: ConfirmOrder) -> None:
|
|
170
|
+
order = await self._repo.get(command.order_id)
|
|
171
|
+
order.confirm()
|
|
172
|
+
await self._repo.save(order)
|
|
173
|
+
for event in order.collect_events():
|
|
174
|
+
await self._bus.publish(event)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Wrong -- generic repository with leaked ORM abstractions:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
# WRONG: this is a leaked ORM, not a domain repository
|
|
181
|
+
class Repository[T]:
|
|
182
|
+
async def filter(self, **kwargs: Any) -> list[T]: ...
|
|
183
|
+
async def all(self) -> list[T]: ...
|
|
184
|
+
|
|
185
|
+
# RIGHT: domain-meaningful methods
|
|
186
|
+
class OrderRepository(Protocol):
|
|
187
|
+
async def get(self, order_id: int) -> Order: ...
|
|
188
|
+
async def get_pending_orders(self) -> list[Order]: ...
|
|
189
|
+
async def save(self, order: Order) -> None: ...
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Domain events
|
|
193
|
+
|
|
194
|
+
Use `@dataclass(frozen=True)` events collected on aggregates. Dispatch via a simple message bus.
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
# domain/events.py
|
|
198
|
+
@dataclass(frozen=True)
|
|
199
|
+
class Event:
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
@dataclass(frozen=True)
|
|
203
|
+
class OrderConfirmed(Event):
|
|
204
|
+
order_id: int
|
|
205
|
+
|
|
206
|
+
# infrastructure/messagebus.py
|
|
207
|
+
EVENT_HANDLERS: dict[type[Event], list[Callable]] = {
|
|
208
|
+
OrderConfirmed: [send_confirmation_email, update_inventory],
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async def handle(event: Event) -> None:
|
|
212
|
+
for handler in EVENT_HANDLERS.get(type(event), []):
|
|
213
|
+
await handler(event)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Domain events are in-process. Integration events cross service boundaries via message queues -- different concern, different infrastructure.
|
|
217
|
+
|
|
218
|
+
## Validation layers
|
|
219
|
+
|
|
220
|
+
| Layer | Responsibility | Tool |
|
|
221
|
+
|-------|---------------|------|
|
|
222
|
+
| **API boundary** | Shape, types, required fields | Pydantic `BaseModel` |
|
|
223
|
+
| **Domain** | Business invariants | Entity/aggregate methods, `__post_init__` |
|
|
224
|
+
| **Cross-aggregate** | Multi-aggregate rules | Domain services |
|
|
225
|
+
|
|
226
|
+
Don't validate the same thing twice. Pydantic checks "is this a valid email string." Domain checks "can this user register with this email given their account state."
|
|
227
|
+
|
|
228
|
+
## Project structure
|
|
229
|
+
|
|
230
|
+
### Pragmatic DDD (start here)
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
src/
|
|
234
|
+
├── ordering/ # Bounded context = package
|
|
235
|
+
│ ├── domain/
|
|
236
|
+
│ │ ├── model.py # Entities, VOs, aggregates
|
|
237
|
+
│ │ ├── events.py # Domain events
|
|
238
|
+
│ │ └── ports.py # Repository protocols
|
|
239
|
+
│ ├── application/
|
|
240
|
+
│ │ └── handlers.py # Command/query handlers (thin)
|
|
241
|
+
│ ├── infrastructure/
|
|
242
|
+
│ │ ├── orm.py # SQLAlchemy mapping
|
|
243
|
+
│ │ └── repository.py # Repository implementations
|
|
244
|
+
│ └── interface/
|
|
245
|
+
│ ├── router.py # FastAPI routes
|
|
246
|
+
│ └── schemas.py # Pydantic request/response
|
|
247
|
+
├── shared/
|
|
248
|
+
│ ├── messagebus.py # Event dispatch
|
|
249
|
+
│ └── uow.py # Unit of Work
|
|
250
|
+
└── main.py
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Import rules: interface -> application -> domain. Infrastructure -> domain (implements ports). Domain imports nothing from other layers.
|
|
254
|
+
|
|
255
|
+
### Simple DDD (CRUD-heavy bounded contexts)
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
src/
|
|
259
|
+
├── notifications/ # Simple context -- no tactical DDD
|
|
260
|
+
│ ├── router.py
|
|
261
|
+
│ ├── schemas.py
|
|
262
|
+
│ ├── service.py # Business logic (thin)
|
|
263
|
+
│ └── models.py # ORM models directly
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Not every bounded context needs full DDD. Apply tactical patterns where business rules are complex.
|
|
267
|
+
|
|
268
|
+
## When to use DDD
|
|
269
|
+
|
|
270
|
+
| Criterion | Use DDD | Skip DDD |
|
|
271
|
+
|-----------|---------|----------|
|
|
272
|
+
| Business invariants | Span multiple entities, enforced transactionally | Single-entity CRUD |
|
|
273
|
+
| Domain complexity | Domain experts disagree on rules | Rules fit on one page |
|
|
274
|
+
| Team size | 3+ developers, domain knowledge distributed | Solo developer, full context in head |
|
|
275
|
+
| Change frequency | Business rules change independently of tech | Schema-driven CRUD, logic is trivial |
|
|
276
|
+
| Bounded contexts | 2+ contexts with different models of same concept | Monolithic domain, single model |
|
|
277
|
+
|
|
278
|
+
## Anti-patterns
|
|
279
|
+
|
|
280
|
+
- **Anemic domain model.** All logic in services, entities are bags of data. You have Transaction Scripts with extra mapping cost.
|
|
281
|
+
- **Repository per entity.** Repositories exist only for aggregate roots. `UserRepository`, `OrderRepository` -- yes. `OrderLineRepository` -- no.
|
|
282
|
+
- **DDD theater.** Using vocabulary (aggregate, bounded context, ubiquitous language) without actually modeling the domain. If your "aggregates" don't enforce any invariants, they're just database records.
|
|
283
|
+
- **Over-abstraction.** `IUserRepositoryFactory`, `AbstractDomainServiceBase` -- Python doesn't need this. Protocol + constructor injection is the ceiling.
|
|
284
|
+
- **Premature event sourcing.** Event sourcing is a persistence strategy, not a default. Use it when you need a full audit log or temporal queries. For everything else, it adds rebuild complexity, eventual consistency headaches, and schema evolution pain.
|
|
285
|
+
- **Wrong aggregate boundaries.** If you load an aggregate and it pulls 50 related entities from the database, the boundary is too wide. If you can't enforce a business rule without loading two aggregates, the boundary is too narrow or the rule belongs in a domain service.
|
|
286
|
+
- **Generic repository.** `Repository[T]` with `.filter()` and `.all()` is a leaked ORM abstraction. Repositories expose domain-meaningful methods: `get_pending_orders()`, not `filter(status="pending")`.
|
|
287
|
+
- **Pydantic in domain layer.** Domain models should be pure Python (dataclasses). Pydantic belongs at system boundaries. Coupling domain logic to Pydantic makes unit testing slower and ties domain evolution to a serialization library.
|
|
288
|
+
- **Importing Java/C# patterns wholesale.** Python has no interfaces, no private fields, no explicit getters/setters. Use Protocol (not Interface), `_convention` (not private fields), properties only when computation is needed.
|