@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.
Files changed (46) hide show
  1. package/.claude/agents/csharp-code-writer.md +32 -0
  2. package/.claude/agents/diagrammer.md +49 -0
  3. package/.claude/agents/frontend-code-writer.md +36 -0
  4. package/.claude/agents/prompt-writer.md +38 -0
  5. package/.claude/agents/python-code-writer.md +32 -0
  6. package/.claude/agents/swift-code-writer.md +32 -0
  7. package/.claude/agents/typescript-code-writer.md +32 -0
  8. package/.claude/commands/git-sync.md +96 -0
  9. package/.claude/commands/project-docs.md +287 -0
  10. package/.claude/settings.json +112 -0
  11. package/.claude/settings.json.default +15 -0
  12. package/.claude/skills/csharp-coding/SKILL.md +368 -0
  13. package/.claude/skills/ddd-architecture-python/SKILL.md +288 -0
  14. package/.claude/skills/feature-driven-architecture-python/SKILL.md +302 -0
  15. package/.claude/skills/gemini-3-prompting/SKILL.md +483 -0
  16. package/.claude/skills/gpt-5-2-prompting/SKILL.md +295 -0
  17. package/.claude/skills/opus-4-6-prompting/SKILL.md +315 -0
  18. package/.claude/skills/plantuml-diagramming/SKILL.md +758 -0
  19. package/.claude/skills/python-coding/SKILL.md +293 -0
  20. package/.claude/skills/react-coding/SKILL.md +264 -0
  21. package/.claude/skills/rest-api/SKILL.md +421 -0
  22. package/.claude/skills/shadcn-ui-coding/SKILL.md +454 -0
  23. package/.claude/skills/swift-coding/SKILL.md +401 -0
  24. package/.claude/skills/tailwind-css-coding/SKILL.md +268 -0
  25. package/.claude/skills/typescript-coding/SKILL.md +464 -0
  26. package/.claude/statusline-command.sh +34 -0
  27. package/.codex/prompts/plan-reviewer.md +162 -0
  28. package/.codex/prompts/project-docs.md +287 -0
  29. package/.codex/skills/ddd-architecture-python/SKILL.md +288 -0
  30. package/.codex/skills/feature-driven-architecture-python/SKILL.md +302 -0
  31. package/.codex/skills/gemini-3-prompting/SKILL.md +483 -0
  32. package/.codex/skills/gpt-5-2-prompting/SKILL.md +295 -0
  33. package/.codex/skills/opus-4-6-prompting/SKILL.md +315 -0
  34. package/.codex/skills/plantuml-diagramming/SKILL.md +758 -0
  35. package/.codex/skills/python-coding/SKILL.md +293 -0
  36. package/.codex/skills/react-coding/SKILL.md +264 -0
  37. package/.codex/skills/rest-api/SKILL.md +421 -0
  38. package/.codex/skills/shadcn-ui-coding/SKILL.md +454 -0
  39. package/.codex/skills/tailwind-css-coding/SKILL.md +268 -0
  40. package/.codex/skills/typescript-coding/SKILL.md +464 -0
  41. package/AGENTS.md +57 -0
  42. package/CLAUDE.md +98 -0
  43. package/LICENSE +201 -0
  44. package/README.md +114 -0
  45. package/bin/cli.mjs +291 -0
  46. 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.