Skip to content

Domain-Driven Design with AI Assistance

Six months into the project, “Order” means three different things across the codebase: a row in orders, a DTO in the checkout service, and a React view-model. The AI keeps inventing fields that don’t exist on the real entity, every refactor quietly reintroduces persistence concerns into the domain layer, and the term your domain experts use in meetings appears nowhere in the code. The model and the language have drifted apart.

Domain-Driven Design fixes that drift by anchoring the code to a shared vocabulary, the Ubiquitous Language. The catch is that DDD demands discipline an AI assistant will happily ignore unless you make the rules explicit and enforce them. Done right, the AI becomes a custodian of your domain knowledge instead of the thing eroding it.

  • Context-file setup for each tool so the AI speaks your domain’s language by default
  • A real round-trip: prompt → generated TypeScript aggregate → how you check it against an invariant
  • Copy-paste prompts to model an aggregate, generate the implementation, and govern bounded-context boundaries
  • The failure modes that show up when DDD meets AI, and how to catch them

The foundation of DDD is the Ubiquitous Language, a vocabulary shared by developers and domain experts. The single highest-leverage DDD move with an AI assistant is to put that vocabulary in the context file the agent reads on every request, so it stops guessing.

The file content is the same idea across tools; only the filename and location differ.

Cursor reads project rules from .cursor/rules/*.mdc. Create .cursor/rules/domain.mdc and set it to always apply so every agent turn in the repo sees the glossary.

The content of that file, regardless of tool, is the actual domain glossary, so a fenced Markdown block is correct here (it is file content, not a prompt):

# Ubiquitous Language — Order Context
- **Order**: a customer's request to purchase products. Aggregate Root.
- **LineItem**: an entry in an Order for a product + quantity. Value Object (immutable).
- **OrderStatus**: state of an Order — Pending | Paid | Shipped | Delivered | Cancelled. Enum.
- **Customer**: the person who placed the Order. Referenced by `CustomerId`, never embedded.
Invariants:
- An Order has at most 10 unique LineItems.
- Status transitions are one-way along the list above (no Shipped → Pending).
- Total is derived from LineItems; never set directly.

Step 2: Model the aggregate, then check the invariant

Section titled “Step 2: Model the aggregate, then check the invariant”

Now ask the AI to design the Order aggregate against that language, and immediately verify it enforces the invariant rather than just compiling.

  1. Ask for the model. Reference the language explicitly so the AI uses your terms, not its defaults.

  2. Read what it generated. A correct addItem rejects the 11th distinct product and recomputes total; LineItem has no setters. If the AI exposed a public setTotal or let status jump backward, the model leaked.

    addItem(productId: ProductId, qty: number): void {
    const distinct = new Set(this.items.map(i => i.productId.value));
    if (!distinct.has(productId.value) && distinct.size >= 10) {
    throw new OrderInvariantError('An order cannot exceed 10 line items');
    }
    // ...merge or push, then recompute derived total
    }
  3. Pin the invariant with a test. Don’t trust prose, ask for the failing-case test so the invariant can’t silently regress later.

The other place AI helps is ongoing architectural governance, catching when one bounded context reaches into another’s internals instead of communicating through events or an anti-corruption layer. This is where the workflow diverges by tool.

Use agent mode for an interactive review while you have the files open. Point it at the boundary you’re worried about.