Domain-Driven Design skill for Claude Code

Code boundaries that match how the business works. Claude starts with strategic DDD before any tactical patterns: it separates core from generic subdomains, finds bounded contexts where words change meaning, writes a glossary per context, and draws a context map with explicit integration patterns. Aggregates come last, kept small around real invariants, and Claude says plainly when CRUD is the better fit.

  • namedomain-driven-design
  • size188 lines · ≈2,100 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Applies domain-driven design where it pays off by classifying subdomains, discovering bounded contexts through event storming and language boundaries, building a ubiquitous language glossary, drawing a context map with integration patterns (anti-corruption layer, open host, conformist), and designing small aggregates that protect real invariants. Covers value objects, domain events, repositories, and when plain CRUD is the better choice. Use when a domain model is tangled, when the same word means different things across teams, when designing aggregates or bounded contexts, or when planning service or module boundaries around business capabilities.

Try asking

  • Our Order model has 60 fields and every team edits it. Help us split it with DDD
  • Find the bounded contexts in this codebase and draw a context map
  • Design the aggregates for our subscription billing domain

Use it when

  • A shared model is tangled and every change ripples
  • The same term means different things to different teams
  • Designing aggregates and their transaction boundaries
  • Planning modules or services around business capabilities

Skip it when

  • The app is simple CRUD with little business logic: keep it simple
  • You need to decide monolith versus microservices specifically: use service-boundaries

How to check the result

Each key term has one meaning per context in the glossary, every context-map arrow names a pattern, and each aggregate maps to one transaction and an invariant you can state.

The common mistake it prevents

Building one large aggregate or one canonical model for everything, which causes lock contention and turns every change into a cross-team negotiation.

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

--- name: domain-driven-design description: Applies domain-driven design where it pays off by classifying subdomains, discovering bounded contexts through event storming and language… license: MIT ---

DDD done well means the code’s boundaries match the business’s boundaries, each context has one meaning per term, and aggregates enforce invariants that the business actually cares about. Applying tactical patterns (repositories, factories, value objects) to a CRUD admin screen is ceremony, not DDD.

Ground rules

  • Strategic design first (subdomains, contexts, language), tactical patterns second.
  • Invest modeling effort in the core subdomain. Buy or keep simple for generic ones.
  • A term with two meanings is two concepts in two contexts. Split the model, do not add a type flag.
  • Aggregates are consistency boundaries, not object graphs. Keep them small; reference other aggregates by ID.
  • Use domain experts’ words in code. If the business says “policy lapses”, the method is lapse(), not setStatus(3).

Workflow

- [ ] 1. Classify subdomains: core, supporting, generic
- [ ] 2. Run an event-storming pass to find events, commands, actors, policies
- [ ] 3. Draw bounded contexts where language and rules change
- [ ] 4. Write the ubiquitous language glossary per context
- [ ] 5. Draw the context map with explicit integration patterns
- [ ] 6. Design aggregates around invariants in the core context
- [ ] 7. Map to code: modules/services, ACLs, events

1. Subdomains

TypeDefinitionInvestment
CoreDifferentiates the business (pricing engine, matching, underwriting)Best engineers, rich model, DDD tactics
SupportingNecessary, specific to you, not differentiating (internal back-office flows)Simple, pragmatic code; CRUD is fine
GenericSolved problem (auth, billing, email, search)Buy or use a library/SaaS

2. Event storming (lightweight, can be done from docs and code)

List in time order, using past-tense business language:

  • Domain events (orange): OrderPlaced, PaymentCaptured, ShipmentDispatched.
  • Commands (blue) that cause them: PlaceOrder, CapturePayment.
  • Actors/systems issuing commands.
  • Policies (“whenever X, then Y”): “When PaymentCaptured, reserve stock.”
  • Read models people need to decide.
  • Hotspots: disagreements, unclear rules, pain points.

Working from an existing codebase: grep for status enums, state transitions, and cron jobs; each transition is a candidate event.

3. Find bounded contexts

Draw a boundary where any of these change:

  • The meaning of a key term (a “Product” in Catalog has descriptions and images; in Inventory it has SKUs, bins, and quantities; in Billing it is a price plan).
  • The people and team who own the rules.
  • The rate of change and the invariants.
  • The lifecycle (an Order in Sales ends at checkout; in Fulfillment it starts there).

Heuristic: if a model needs many optional fields only some flows use, it is probably two models merged.

4. Ubiquitous language glossary

## Context: Fulfillment
| Term | Meaning | Not to be confused with |
|---|---|---|
| Shipment | A set of order lines packed into one parcel with one carrier label | Order (Sales) |
| Backorder | Lines accepted but not reservable from current stock | Cancellation |
| Dispatch | Handing the parcel to the carrier; irreversible | Packing |

Use these names in code, tests, APIs, and conversation. Rename code that disagrees.

5. Context map

PatternUse whenImplementation
PartnershipTwo teams succeed or fail together, coordinate closelyShared planning; joint releases
Shared kernelSmall shared model owned jointlyShared library with strict review; keep tiny
Customer–supplierDownstream needs drive upstream prioritiesUpstream provides API; contract tests
ConformistDownstream adopts upstream model as is (no influence upstream)Use their types directly; accept coupling
Anti-corruption layer (ACL)Upstream model is messy or legacy; protect yoursTranslator/adapter mapping their model to yours
Open host service + published languageMany consumersVersioned public API/events with documented schema
Separate waysIntegration costs more than duplicationNo integration; duplicate small functionality

Draw it as a diagram (Mermaid or C4, see c4-diagrams) with an arrow per relationship labeled with the pattern and direction (upstream U / downstream D).

6. Aggregates

Rules (after Vaughn Vernon):

  1. Model true invariants in consistency boundaries: an aggregate contains exactly what must be consistent in one transaction.
  2. Design small aggregates. Start with one entity plus value objects.
  3. Reference other aggregates by identity only.
  4. Use eventual consistency (domain events) across aggregates.
// Value object: immutable, equality by value, validates itself
export class Money {
  private constructor(readonly cents: number, readonly currency: 'USD' | 'EUR') {}
  static of(cents: number, currency: 'USD' | 'EUR') {
    if (!Number.isInteger(cents) || cents < 0) throw new Error('invalid amount');
    return new Money(cents, currency);
  }
  add(o: Money) { if (o.currency !== this.currency) throw new Error('currency mismatch'); return Money.of(this.cents + o.cents, this.currency); }
}

// Aggregate root: all changes go through it; it protects invariants and records events
export class Order {
  private events: DomainEvent[] = [];
  private constructor(readonly id: OrderId, private lines: OrderLine[], private status: 'draft' | 'placed' | 'cancelled', readonly customerId: CustomerId) {}

  addLine(productId: ProductId, qty: number, unitPrice: Money) {
    if (this.status !== 'draft') throw new DomainError('order is not editable');
    if (this.lines.length >= 50) throw new DomainError('max 50 lines');
    this.lines.push(new OrderLine(productId, qty, unitPrice));
  }

  place() {
    if (this.lines.length === 0) throw new DomainError('cannot place empty order');
    this.status = 'placed';
    this.events.push({ type: 'OrderPlaced', orderId: this.id, total: this.total(), at: new Date() });
  }

  total(): Money { return this.lines.reduce((sum, l) => sum.add(l.subtotal()), Money.of(0, 'USD')); }
  pullEvents() { const e = this.events; this.events = []; return e; }
}
  • One repository per aggregate root (OrderRepository.save(order)), never per entity.
  • One aggregate per transaction. If a use case must change two aggregates atomically, either the boundary is wrong or the second change should be eventual (event + handler).
  • Persist domain events in the same transaction as the aggregate (outbox; see event-driven-architecture).

Signs of a wrong aggregate boundary: frequent optimistic-lock conflicts (too big), or invariants enforced by application services checking several aggregates (too small or wrong cut).

7. Map to code

  • One bounded context → one module (see modular-monolith) or one service (see service-boundaries). Not one service per aggregate.
  • Domain layer has no framework or ORM imports where practical (see hexagonal-architecture).
  • Cross-context calls go through the published API or events, with an ACL on the consuming side when models differ.

When not to use DDD tactics

  • CRUD over a form with no rules beyond validation.
  • Reporting and analytics read paths (use read models or SQL directly).
  • Generic subdomains handled by a vendor.

Gotchas

  • “Entity” from the ORM is not a domain entity; anemic models with all logic in services usually mean the rules were never captured.
  • A shared common or core package that every context imports becomes an accidental shared kernel that nobody owns.
  • One giant canonical data model for the whole company (“enterprise model”) fights bounded contexts; let each context own its model and translate at the edges.
  • Event names in technical language (OrderUpdated) hide intent; prefer OrderAddressCorrected.

Output

**Subdomains:** core = pricing, matching; supporting = onboarding; generic = billing (Stripe)
**Bounded contexts:** Catalog, Pricing, Ordering, Fulfillment (map: docs/context-map.md)
**Glossary:** docs/glossary/<context>.md — 14 terms; "Product" split into 3 concepts
**Aggregates (Ordering):** Order (lines, status; invariant: placed orders immutable), Cart
**Integration:** Fulfillment consumes OrderPlaced via ACL; Pricing is open host with versioned API
**Open questions:** who owns refunds — Ordering or Billing?

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc