Domain-Driven Design skill for Codex
Model the business rules where they belong. Codex first decides whether a domain is complex enough for tactical DDD, then harvests the language from code and tickets to find bounded contexts and draw a context map. For the core domain it designs small aggregates around stated invariants, replaces primitives with value objects, raises domain events after commit, and pins every rule with a test while refactoring an anemic model step by step.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- domain-driven-design Installs to ~/.agents/skills/domain-driven-design/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install domain-driven-design The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project domain-driven-design Installs to .agents/skills/domain-driven-design/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/domain-driven-design.zipunzip -o domain-driven-design.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) domain-driven-design PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $domain-driven-design, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Applies domain-driven design to real codebases - discovers bounded contexts and a context map, builds a ubiquitous language glossary, and models aggregates, entities, value objects, domain events and repositories with invariants enforced in code. Use when a domain model is anemic or tangled, when splitting a monolith or naming services, when terms mean different things in different teams, when designing aggregates and transaction boundaries, or when running event storming notes into code.
Use it when
- Business rules are scattered across services, controllers and SQL
- The same term means different things to different teams or modules
- You are deciding transaction and consistency boundaries for a complex domain
- Event storming output needs to become code
Use something else when
- You need to decide how to split deployable services: use microservice-boundaries
- You want ports-and-adapters layering around an existing model: use hexagonal-architecture
How to check the result
Each invariant has an aggregate-level test that fails when the rule is removed, no code outside the aggregate mutates its state, and each transaction saves at most one aggregate.
The mistake it prevents
Designing one huge aggregate that holds everything about a customer, which causes lock contention and slow loads instead of protecting any real rule.
DDD done well shows up as code where the business rules live in one obvious place, each term means exactly one thing inside its context, and transaction boundaries match the invariants the business actually cares about. It is a modelling discipline, not a folder structure: apply the tactical patterns only where the domain is complex enough to pay.
Decide how much DDD the problem needs
| Subdomain type | Example | Approach |
|---|---|---|
| Core (competitive advantage, complex rules) | Pricing engine, underwriting, routing | Full tactical DDD: aggregates, value objects, events, rich model |
| Supporting (needed, specific, not differentiating) | Internal back-office workflows | Simple domain model or transaction scripts |
| Generic (solved problem) | Auth, billing, email, search | Buy or use a library; wrap it behind an anti-corruption layer |
If the “domain” is CRUD over forms with validation, say so and recommend a plain service + ORM design. Aggregates on a CRUD app add ceremony without benefit.
Workflow
- [ ] 1. Read AGENTS.md, the main modules, and the database schema
- [ ] 2. Harvest the language: nouns and verbs from code, UI, tickets, docs
- [ ] 3. Find where the same word means different things -> context boundaries
- [ ] 4. Draw the context map with relationships between contexts
- [ ] 5. For the core context, list invariants and design aggregates around them
- [ ] 6. Model value objects, entities, domain events; move rules into them
- [ ] 7. Add tests that pin each invariant; refactor callers incrementally
Strategic design
Ubiquitous language
Build a glossary per context, from evidence (rg -n "class |type |interface ", table
names, UI labels, support tickets). Keep it in the repo, e.g. docs/domain/glossary.md:
| Term (Fulfilment context) | Meaning | Not to be confused with |
|---|---|---|
| Shipment | Physical parcel leaving a warehouse; one order can produce several | Order (Sales context) |
| Allocation | Reservation of stock units at a specific warehouse for a shipment | Hold (Sales: a soft cart reservation) |
Signals of a context boundary: the same word with different attributes or rules
(Customer in Sales has a credit limit, in Support has a ticket history), different
teams owning rules, different rates of change, different consistency needs.
Context map
| Relationship | Meaning | Implementation hint |
|---|---|---|
| Customer / Supplier | Downstream needs drive upstream’s roadmap | Versioned API or events owned by upstream |
| Conformist | Downstream adopts upstream’s model as is | Fine for generic subdomains |
| Anti-corruption layer (ACL) | Downstream translates upstream’s model into its own | Adapter + mapper at the boundary; never leak upstream types inward |
| Open host service / published language | Upstream offers a stable public protocol | Documented API or event schema |
| Shared kernel | Two contexts share a small model and co-own it | Keep tiny; needs joint ownership |
| Separate ways | No integration | Duplicate the little data you need |
Record the map as a short Markdown table or a C4-style diagram ($c4-architecture-diagrams).
Tactical design
Aggregates
An aggregate is the consistency boundary: everything inside changes in one transaction and its invariants always hold after every command.
Design rules:
- Start from invariants: “an order’s total must not exceed the customer’s approved credit at placement” or “a seat can be booked by only one passenger per flight”.
- Make the aggregate the smallest cluster that can enforce that invariant alone.
- Reference other aggregates by ID, never by object reference.
- Modify one aggregate per transaction. Cross-aggregate rules become eventually consistent via domain events, or the boundary is wrong.
- Load and save whole aggregates through a repository; no repositories for internal entities.
- Protect concurrency with a version column (optimistic locking).
from dataclasses import dataclass, field
from decimal import Decimal
from functools import reduce
from uuid import UUID
@dataclass(frozen=True)
class Money:
amount: Decimal
currency: str
def __post_init__(self):
if self.amount < 0:
raise ValueError("Money cannot be negative")
def add(self, other: "Money") -> "Money":
if other.currency != self.currency:
raise ValueError("Currency mismatch")
return Money(self.amount + other.amount, self.currency)
@dataclass
class Order: # aggregate root
id: UUID
customer_id: UUID # reference by ID, not a Customer object
credit_limit: Money
lines: list["OrderLine"] = field(default_factory=list)
status: str = "draft"
version: int = 0
events: list[object] = field(default_factory=list, repr=False)
def add_line(self, sku: str, qty: int, unit_price: Money) -> None:
if self.status != "draft":
raise DomainError("Only draft orders can change")
if qty <= 0:
raise DomainError("Quantity must be positive")
new_total = self.total().add(Money(unit_price.amount * qty, unit_price.currency))
if new_total.amount > self.credit_limit.amount:
raise DomainError("Order would exceed approved credit")
self.lines.append(OrderLine(sku, qty, unit_price))
def place(self) -> None:
if not self.lines:
raise DomainError("Cannot place an empty order")
self.status = "placed"
self.events.append(OrderPlaced(order_id=self.id, total=self.total()))
def total(self) -> Money:
zero = Money(Decimal("0"), self.credit_limit.currency)
return reduce(Money.add, (line.subtotal() for line in self.lines), zero)
OrderLine is a value object with subtotal(), OrderPlaced a frozen dataclass, and
DomainError a plain exception the application layer maps to a 4xx/409.
Building blocks
| Block | Identity? | Mutable? | Use for |
|---|---|---|---|
| Entity | Yes | Yes | Things tracked over time (Order, Shipment) |
| Value object | No (equal by value) | No | Money, Address, DateRange, Sku, EmailAddress |
| Domain event | Event ID | No | Facts in past tense: OrderPlaced, ShipmentDispatched |
| Domain service | No | Stateless | Rules spanning several aggregates that fit none of them |
| Repository | - | - | Load/save one aggregate type; interface in the domain, implementation in infrastructure |
| Factory | - | - | Complex creation with invariants |
Prefer value objects aggressively: replacing str/Decimal primitives with Money,
Sku, Percentage removes whole classes of bugs and is the cheapest DDD win.
Domain events
- Raise them inside the aggregate (collected in a list), publish after the transaction commits. Publishing before commit announces things that may roll back; use the outbox ($outbox-pattern) for reliable delivery.
- Name them in business language, past tense; carry IDs plus the facts consumers need.
- Internal domain events can change freely; events published to other contexts are a contract and need versioning ($event-driven-architecture).
Refactoring an anemic model
- Find rule-heavy service methods:
rg -n "if .*status|raise|throw" services/. - Write characterisation tests around the current behaviour.
- Move one rule at a time into the entity method that owns the state it checks.
- Replace setters used for state transitions with intention-revealing methods
(
order.cancel(reason)instead oforder.status = "cancelled"). - Introduce value objects for primitives that carry rules.
- Leave the ORM mapping alone at first; many ORMs can map rich entities directly (SQLAlchemy imperative mapping, JPA embeddables, EF Core owned types).
Gotchas
- Large aggregates (“Customer with all orders”) cause lock contention and slow loads. If two users routinely conflict on unrelated changes, the aggregate is too big.
- Cross-aggregate invariants enforced with multi-aggregate transactions creep back in. Challenge the rule with the domain expert: most are fine eventually consistent.
- One shared
UserorProductmodel used by every module is the opposite of bounded contexts. Each context keeps only the attributes it needs. - Generic repositories (
Repository<T>with arbitrary queries) leak persistence into the domain. Query needs for screens belong in a read side, not the aggregate repository. - Folder names like
domain/,application/do not make a design DDD. Check where the rules actually live.
Verification
- Each invariant has a unit test on the aggregate that fails if the rule is removed.
- No code outside the aggregate mutates its internal state (
rgfor direct field writes). - Each transaction saves at most one aggregate; list exceptions and justify them.
- Glossary terms match class, table and API names in that context.
Output contract
**Subdomains:** core: <..>; supporting: <..>; generic: <..>
**Bounded contexts and map:** <table or diagram path>
**Glossary:** docs/domain/glossary.md (<n> terms, <k> conflicts resolved)
**Aggregates (core):** <name> - invariants: <list>; references: <IDs>
**Code changes:** <files>, tests added: <list>
**Open questions for domain experts:** <list> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Domain-Driven Design"
short_description: "Model the business rules where they belong"
default_prompt: "$domain-driven-design Our Order service is a 2,000-line class of if statements - help me move the rules into a proper domain model" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.