prd-to-ddd-design
Skill0 starsChanged 3 months agoMITView on GitHub
Domain-Driven Design decomposition with Event Storming
In this repository27 other files it gives its agents.
- architecture-designerSkill
- architecture-patternsSkill
- code-reviewSkill
- dockerSkill
- fastapiSkill
- feature-specificationSkill
- github-issue-resolverSkill
- github-opsSkill
- kubernetes-devopsSkill
- monitoringSkill
- nextjsSkill
- nginxSkill
- prd-writerSkill
- prismaSkill
- react-expertSkill
- redisSkill
- redis-storeSkill
- requirements-analysisSkill
- shadcn-uiSkill
- ssh-essentialsSkill
- task-decomposerSkill
- task-development-workflowSkill
- trpc-best-practicesSkill
- typescript-proSkill
- typescriptSkill
- webhookSkill
- zodSkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 6 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈6
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 25
Use this skill when you need to:
- Distinctive: line 158
Karpathy Principle: Surgical Changes Over Rewrites: - Start with one large aggregate.
- Distinctive: line 343
Karpathy Principle: Simplicity First: - Only use Specification pattern when rules are complex and reused.
- Distinctive: line 506
Start with large aggregates
- Distinctive: line 576
Use different models in different contexts
- Distinctive: line 642
Before considering design complete:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
PRD to DDD Design
Purpose: Translate Product Requirements Documents into Domain-Driven Design models with bounded contexts, aggregates, and domain events.
Works with: Any programming language, any architecture
License: MIT (original work, inspired by Eric Evans DDD, Vaughn Vernon, Event Storming)
When to Use
Use this skill when you need to:
- Map business requirements to code structure
- Identify bounded contexts for microservices
- Design aggregates and entities
- Define domain events
- Prevent anemic domain model
Don't use this for:
- Simple CRUD apps (overkill)
- Well-understood domains (just code it)
- Prototypes (defer complexity)
Prerequisites
1. Read the PRD
Think Before Coding (Karpathy Principle #1):
- What's the core domain?
- What are the key business rules?
- What are the invariants (things that must always be true)?
2. Understand DDD Building Blocks
Core concepts:
- Entity: Object with unique identity
- Value Object: Object defined by attributes (no identity)
- Aggregate: Cluster of entities + value objects (consistency boundary)
- Domain Event: Something that happened in the domain
- Bounded Context: Semantic boundary (different models for different contexts)
Core Workflows
Step 1: Identify Bounded Contexts
What are the distinct areas of the business?
Example (E-commerce):
Bounded Contexts:
1. Sales (shopping cart, checkout)
2. Inventory (stock management)
3. Shipping (fulfillment, tracking)
4. Customer (profiles, preferences)
5. Billing (invoices, payments)Test: Can these contexts have different definitions of the same word?
"Order" in Sales context: Shopping cart + checkout
"Order" in Shipping context: Package to be shipped
"Order" in Billing context: Invoice to be paid
Different meanings → Different bounded contexts ✅Karpathy Principle: Model the Domain, Not the Database - Bounded contexts reflect business thinking, not database tables. If two teams talk about "orders" differently, they're in different contexts. Model that reality.
Step 2: Identify Aggregates
What are the consistency boundaries?
Aggregate = cluster of related objects that must stay consistent
Example:
// Order Aggregate
class Order {
id: OrderId // Entity (has identity)
customerId: CustomerId
items: OrderItem[] // Value Objects (no identity)
status: OrderStatus
total: Money // Value Object
// Business rules (invariants)
addItem(product: Product, quantity: number) {
if (this.status !== 'DRAFT') {
throw new Error('Cannot modify confirmed order')
}
if (quantity <= 0) {
throw new Error('Quantity must be positive')
}
this.items.push(new OrderItem(product, quantity))
this.recalculateTotal()
}
confirm() {
if (this.items.length === 0) {
throw new Error('Cannot confirm empty order')
}
this.status = 'CONFIRMED'
// Emit domain event
this.addDomainEvent(new OrderConfirmedEvent(this.id, this.total))
}
private recalculateTotal() {
this.total = this.items.reduce((sum, item) => sum + item.price, Money.zero())
}
}
// Value Objects
class OrderItem {
constructor(
public readonly product: Product,
public readonly quantity: number,
public readonly price: Money
) {}
}
class Money {
constructor(public readonly amount: number, public readonly currency: string) {}
static zero() {
return new Money(0, 'USD')
}
}Aggregate Design Rules:
- Small aggregates (prefer single entity + value objects)
- Reference other aggregates by ID only (not direct references)
- Enforce invariants within aggregate
- One aggregate per transaction
Karpathy Principle: Surgical Changes Over Rewrites - Start with one large aggregate. Split only when you hit real consistency or performance issues. Premature splitting creates distributed transaction problems.
Step 3: Define Domain Events
What happens in the domain?
Events = past-tense facts
// Domain Events
class OrderConfirmedEvent {
constructor(
public readonly orderId: OrderId,
public readonly total: Money,
public readonly occurredAt: Date = new Date()
) {}
}
class PaymentReceivedEvent {
constructor(
public readonly orderId: OrderId,
public readonly amount: Money,
public readonly occurredAt: Date = new Date()
) {}
}
class OrderShippedEvent {
constructor(
public readonly orderId: OrderId,
public readonly trackingNumber: string,
public readonly occurredAt: Date = new Date()
) {}
}Use events for:
- Cross-aggregate communication
- Triggering side effects
- Audit trail
- Event sourcing
Step 4: Map Context Relationships
How do contexts interact?
Patterns:
- Shared Kernel: Two contexts share code
- Customer/Supplier: Upstream defines API, downstream consumes
- Conformist: Downstream accepts upstream model
- Anti-Corruption Layer: Translate between contexts
Example:
Sales (Upstream) → Inventory (Downstream)
When order is confirmed, reserve inventory
Inventory → Shipping
When stock allocated, create shipment
Shipping → Customer
When shipped, send tracking emailCommon Patterns
Pattern 1: Anemic vs Rich Domain Model
❌ Anemic (just data, no behavior):
class Order {
id: string
items: OrderItem[]
status: string
total: number
}
// Business logic in service layer
class OrderService {
addItem(order: Order, product: Product, quantity: number) {
order.items.push({ product, quantity, price: product.price * quantity })
order.total = order.items.reduce((sum, item) => sum + item.price, 0)
}
}✅ Rich (behavior + data):
class Order {
private items: OrderItem[] = []
private status: OrderStatus
addItem(product: Product, quantity: number) {
this.guardCanModify()
this.items.push(OrderItem.create(product, quantity))
this.recalculateTotal()
}
private guardCanModify() {
if (this.status !== 'DRAFT') {
throw new Error('Cannot modify confirmed order')
}
}
}Goal-Driven Execution (Karpathy Principle #4): Business rules live in domain, not services.
Pattern 2: Repository Pattern
Separate domain from persistence:
// Domain interface (port)
interface OrderRepository {
save(order: Order): Promise<void>
findById(id: OrderId): Promise<Order | null>
findByCustomer(customerId: CustomerId): Promise<Order[]>
}
// Infrastructure adapter
class PostgresOrderRepository implements OrderRepository {
async save(order: Order) {
// Map domain object → database rows
await db.query('INSERT INTO orders ...', order.toData())
}
async findById(id: OrderId) {
const row = await db.query('SELECT * FROM orders WHERE id = $1', [id.value])
if (!row) return null
// Map database row → domain object
return Order.fromData(row)
}
}Pattern 3: Specification Pattern
Encapsulate business rules as reusable objects:
// Specification interface
interface Specification<T> {
isSatisfiedBy(candidate: T): boolean
}
// Concrete specifications
class PremiumCustomerSpecification implements Specification<Customer> {
isSatisfiedBy(customer: Customer): boolean {
return customer.totalSpent.isGreaterThan(Money.fromDollars(10000))
}
}
class EligibleForDiscountSpecification implements Specification<Order> {
constructor(private customer: Customer) {}
isSatisfiedBy(order: Order): boolean {
const isPremium = new PremiumCustomerSpecification().isSatisfiedBy(this.customer)
const isLargeOrder = order.total.isGreaterThan(Money.fromDollars(500))
return isPremium || isLargeOrder
}
}
// Usage in domain
class OrderService {
calculateDiscount(order: Order, customer: Customer): Money {
const spec = new EligibleForDiscountSpecification(customer)
if (spec.isSatisfiedBy(order)) {
return order.total.multiply(0.1) // 10% discount
}
return Money.zero()
}
}Karpathy Principle: Simplicity First - Only use Specification pattern when rules are complex and reused. For simple one-off checks, inline the logic.
Pattern 4: Domain Services
Operations that don't naturally belong to an entity:
// Domain service for complex operations
class PricingService {
calculatePrice(
product: Product,
quantity: number,
customer: Customer
): Money {
let price = product.basePrice.multiply(quantity)
// Apply volume discount
if (quantity >= 10) {
price = price.multiply(0.9) // 10% off
}
// Apply customer tier discount
if (customer.tier === 'GOLD') {
price = price.multiply(0.95) // 5% off
}
return price
}
}
// Used by aggregate
class Order {
addItem(
product: Product,
quantity: number,
pricingService: PricingService,
customer: Customer
) {
const price = pricingService.calculatePrice(product, quantity, customer)
this.items.push(new OrderItem(product, quantity, price))
}
}Use domain services when:
- Operation spans multiple aggregates
- Logic doesn't naturally fit one entity
- Stateless operations (no identity needed)
Pattern 5: Event Sourcing
Store state changes as events instead of current state:
// Event store
class OrderEventStore {
private events: DomainEvent[] = []
save(aggregate: Order) {
const newEvents = aggregate.uncommittedEvents()
this.events.push(...newEvents)
aggregate.markEventsAsCommitted()
}
load(orderId: OrderId): Order {
const events = this.events.filter(e => e.aggregateId.equals(orderId))
return Order.fromEvents(events)
}
}
// Aggregate rebuilt from events
class Order {
private items: OrderItem[] = []
private status: OrderStatus = 'DRAFT'
private uncommitted: DomainEvent[] = []
static fromEvents(events: DomainEvent[]): Order {
const order = new Order()
events.forEach(event => order.apply(event))
return order
}
private apply(event: DomainEvent) {
if (event instanceof OrderItemAddedEvent) {
this.items.push(event.item)
} else if (event instanceof OrderConfirmedEvent) {
this.status = 'CONFIRMED'
}
}
addItem(product: Product, quantity: number) {
const event = new OrderItemAddedEvent(this.id, product, quantity)
this.apply(event)
this.uncommitted.push(event)
}
}Use event sourcing when:
- Full audit trail required
- Time-travel debugging needed
- Multiple projections of same events
- CQRS architecture
Common Pitfalls
Pitfall 1: Too Many Aggregates
❌ Bad - Every entity is an aggregate:
class Order {
id: OrderId
customerId: CustomerId // Reference to Customer aggregate
items: OrderItemId[] // Reference to OrderItem aggregates (WRONG!)
}
class OrderItem {
id: OrderItemId
orderId: OrderId
productId: ProductId
quantity: number
}Why it's wrong:
- Cannot add item + recalculate total in one transaction
- Complex coordination between Order and OrderItem aggregates
- Performance issues (multiple saves per operation)
✅ Good - OrderItem as value object:
class Order {
id: OrderId
customerId: CustomerId
items: OrderItem[] // Value objects within aggregate
addItem(product: Product, quantity: number) {
this.items.push(new OrderItem(product, quantity))
this.recalculateTotal()
}
}
class OrderItem {
// No id! Value object
constructor(
public readonly product: Product,
public readonly quantity: number
) {}
}Symptoms:
- Complex transactions across aggregates
- Performance issues (too much locking)
- Confusing consistency boundaries
How to avoid:
- Start with large aggregates
- Split only when needed
- Most aggregates should be single entity + value objects
Simplicity First (Karpathy Principle #2): Prefer fewer, larger aggregates.
Pitfall 2: Ignoring Bounded Contexts
❌ Bad - One "Order" for everything:
// God object Order
class Order {
// Sales context
cart: CartItem[]
checkout(): void {}
// Inventory context
reservedStock: Map<ProductId, number>
allocateStock(): void {}
// Shipping context
trackingNumber: string
ship(): void {}
// Billing context
invoice: Invoice
generateInvoice(): void {}
}Why it's wrong:
- Tight coupling (changing shipping affects sales)
- Different teams stepping on each other
- Cannot deploy contexts independently
✅ Good - Separate models per context:
// Sales context
class SalesOrder {
items: OrderItem[]
confirm() {
// Emit event
publish(new OrderConfirmedEvent(this.id))
}
}
// Inventory context
class StockReservation {
orderId: OrderId
items: Map<ProductId, number>
}
// Shipping context
class Shipment {
orderId: OrderId
trackingNumber: string
}
// Each context listens to events from othersSymptoms:
- God objects (Order knows everything)
- Tight coupling between features
- Difficult to change one area without breaking others
How to avoid:
- Identify distinct business areas
- Use different models in different contexts
- Accept some duplication across contexts
Pitfall 3: Not Using Value Objects
❌ Bad - Primitives everywhere:
class Order {
totalAmount: number
currency: string
addDiscount(percent: number) {
this.totalAmount = this.totalAmount * (1 - percent)
}
}
// Easy to make mistakes
order.addDiscount(0.1) // 10% or 1%? Unclear!
order.totalAmount = -100 // Negative price allowed!✅ Good - Value objects enforce invariants:
class Money {
constructor(
private readonly amount: number,
private readonly currency: string
) {
if (amount < 0) throw new Error('Money cannot be negative')
if (!['USD', 'EUR', 'GBP'].includes(currency)) {
throw new Error('Invalid currency')
}
}
multiply(factor: number): Money {
return new Money(this.amount * factor, this.currency)
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error('Cannot add different currencies')
}
return new Money(this.amount + other.amount, this.currency)
}
}
class Order {
total: Money
addDiscount(percentage: Percentage) {
this.total = this.total.multiply(1 - percentage.value)
}
}Use value objects for:
- Money, dates, email addresses
- Coordinates, addresses, phone numbers
- Any concept with validation rules
Verification Checklist
Before considering design complete:
Domain Model:
- Bounded contexts identified
- Aggregates defined with clear boundaries
- Business rules in domain, not services
- Value objects used where appropriate
- Entities have identity, value objects don't
Events:
- Domain events defined (past tense)
- Events published on state changes
- Cross-aggregate communication via events
- Event handler side effects isolated
Persistence:
- Repository interfaces defined
- Domain independent of database
- No ORM annotations in domain entities
Testing:
- Domain logic unit tested (no mocks needed)
- Aggregates testable in isolation
- Events captured and verifiable
Integration with Other Skills
Combine with:
prd-writer- Start with PRDarchitecture-patterns- Apply hexagonal or event-driven architecturetask-decomposer- Break design into tasks
Workflow:
# 1. Write PRD (prd-writer)
# 2. Map to DDD (prd-to-ddd-design)
# 3. Choose architecture (architecture-patterns)
# 4. Implement (task-development-workflow)References
Books:
- Eric Evans - Domain-Driven Design: https://www.domainlanguage.com/ddd/
- Vaughn Vernon - Implementing Domain-Driven Design: https://vaughnvernon.com/
- Event Storming: https://www.eventstorming.com/
Patterns:
- Specification Pattern: https://martinfowler.com/apsupp/spec.pdf
- Event Sourcing: https://martinfowler.com/eaaDev/EventSourcing.html
- CQRS: https://martinfowler.com/bliki/CQRS.html
Related Skills:
prd-writer- Product requirementsarchitecture-patterns- Architecture selectiontask-decomposer- Task breakdown
Meta: Skill Quality
Incorporates Karpathy Principles:
- ✅ Think Before Coding - Model domain before implementation
- ✅ Simplicity First - Start with simple aggregates, add complexity when needed
- ✅ Surgical Changes - Split aggregates only when hitting real problems
- ✅ Goal-Driven - Business rules define domain
- ✅ Model the Domain - Reflect business thinking, not database structure
- ✅ Understand the Problem - Use Specification pattern only for complex, reused rules
Tested With:
- DDD in TypeScript, C#, Java
- Event-driven architectures
- Microservices with bounded contexts
- Event sourcing and CQRS
Completeness: 9/10 - Covers core DDD patterns, event sourcing, and common pitfalls. Missing: Saga pattern, process managers, complex event flows.
Last Updated: April 13, 2026
Maintainer: Alteriom
License: MIT
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

