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.

  • Architecture
  • $domain-driven-design
  • 203 lines
  • 2 files
  • ~2,350 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- domain-driven-design

Installs to ~/.agents/skills/domain-driven-design/ for every project on this machine.

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

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.

~/.agents/skills/domain-driven-design/SKILL.md

--- name: domain-driven-design description: Applies domain-driven design to real codebases - discovers bounded contexts and a context map, builds a ubiquitous language glossary, and models… license: MIT ---

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 typeExampleApproach
Core (competitive advantage, complex rules)Pricing engine, underwriting, routingFull tactical DDD: aggregates, value objects, events, rich model
Supporting (needed, specific, not differentiating)Internal back-office workflowsSimple domain model or transaction scripts
Generic (solved problem)Auth, billing, email, searchBuy 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

RelationshipMeaningImplementation hint
Customer / SupplierDownstream needs drive upstream’s roadmapVersioned API or events owned by upstream
ConformistDownstream adopts upstream’s model as isFine for generic subdomains
Anti-corruption layer (ACL)Downstream translates upstream’s model into its ownAdapter + mapper at the boundary; never leak upstream types inward
Open host service / published languageUpstream offers a stable public protocolDocumented API or event schema
Shared kernelTwo contexts share a small model and co-own itKeep tiny; needs joint ownership
Separate waysNo integrationDuplicate 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:

  1. 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”.
  2. Make the aggregate the smallest cluster that can enforce that invariant alone.
  3. Reference other aggregates by ID, never by object reference.
  4. Modify one aggregate per transaction. Cross-aggregate rules become eventually consistent via domain events, or the boundary is wrong.
  5. Load and save whole aggregates through a repository; no repositories for internal entities.
  6. 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

BlockIdentity?Mutable?Use for
EntityYesYesThings tracked over time (Order, Shipment)
Value objectNo (equal by value)NoMoney, Address, DateRange, Sku, EmailAddress
Domain eventEvent IDNoFacts in past tense: OrderPlaced, ShipmentDispatched
Domain serviceNoStatelessRules 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

  1. Find rule-heavy service methods: rg -n "if .*status|raise|throw" services/.
  2. Write characterisation tests around the current behaviour.
  3. Move one rule at a time into the entity method that owns the state it checks.
  4. Replace setters used for state transitions with intention-revealing methods (order.cancel(reason) instead of order.status = "cancelled").
  5. Introduce value objects for primitives that carry rules.
  6. 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 User or Product model 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 (rg for 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.yamlgenerated from this page

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.

esc