| name | domain-driven-design |
| description | Apply Domain-Driven Design to model complex business domains. Outputs bounded contexts, aggregates, domain events, ubiquitous language glossary, and context maps for production systems. |
| argument-hint | ["business domain","core subdomains","team structure","integration points"] |
| allowed-tools | Read, Write |
Domain-Driven Design (DDD)
DDD is a software design approach that aligns code structure with business reality. Not an architecture pattern — a way of thinking that produces maintainable systems by putting the domain model at the centre.
Use DDD when: business logic is complex, multiple teams own different parts of the system, or the codebase has become a tangled mess of anemic models and service classes that nobody fully understands.
Skip DDD when: the system is primarily CRUD with simple rules, the team is small with a single bounded context, or time pressure prevents the discovery investment.
Process
- Event storm the domain. Run a collaborative session with domain experts and developers. Put domain events (orange stickies) on a wall in time order. Add commands, aggregates, policies, and read models. Surface the language experts actually use.
- Identify bounded contexts. Cluster events and concepts where the language is consistent. A bounded context is a linguistic boundary — the same word means the same thing everywhere inside it.
- Draw the context map. Show how bounded contexts relate: Shared Kernel, Customer/Supplier, Conformist, Anti-Corruption Layer, Open Host Service, Published Language.
- Define aggregates. Within each context, find the consistency boundaries. An aggregate is a cluster of objects treated as a single unit for data changes. Every aggregate has a root entity.
- Model domain events. Define the events that flow between aggregates and contexts. These become the integration contracts.
- Write the ubiquitous language glossary. Document the agreed terms. Update code to match — class names, method names, variable names should reflect domain language exactly.
- Implement the tactical patterns. Entities, Value Objects, Aggregates, Domain Services, Repositories, Factories, Domain Events.
- Protect boundaries. Use Anti-Corruption Layers to translate between contexts. Never let domain models leak across context boundaries.
Bounded Context Identification
Event Storming output → cluster by consistent language
GOOD boundary signals:
- Same word, different meaning in two areas → separate context
- Team owns end-to-end → natural context boundary
- Different rate of change → separate context
- Clear upstream/downstream relationship → context map seam
BAD boundary signals:
- Based on technology (front-end vs back-end)
- Based on team org chart alone (Conway's Law trap)
- Too fine-grained (one aggregate per context)
Context Map Patterns
| Pattern | When to Use | Integration Approach |
|---|
| Shared Kernel | Two teams share a small, stable model | Joint ownership, versioned, approval required |
| Customer/Supplier | Upstream/downstream with negotiated contract | Downstream specifies needs, upstream commits |
| Conformist | Upstream has no interest in downstream needs | Downstream conforms to upstream model |
| Anti-Corruption Layer (ACL) | Integrating with legacy or external system | Translate at the boundary, never pollute domain |
| Open Host Service | Upstream serves many downstreams | Publish a well-documented protocol |
| Published Language | Multiple teams, complex integration | Shared schema (e.g. JSON Schema, Protobuf) |
Example Context Map — E-Commerce Platform:
┌─────────────────┐ ┌─────────────────┐
│ Order Context │────────▶│Inventory Context│
│ (Customer) │ C/S │ (Supplier) │
└────────┬────────┘ └─────────────────┘
│ ▲
│ ACL │ OHS
▼ │
┌─────────────────┐ ┌─────────────────┐
│ Payment Context│ │ Warehouse Legacy │
│ (Conformist) │─────────│ ERP System │
└─────────────────┘ ACL └─────────────────┘
Aggregate Design
class Order:
def __init__(self):
self.items = []
self.status = "pending"
self.total = 0
class OrderService:
def add_item(self, order, item, qty):
order.items.append({"item": item, "qty": qty})
order.total += item.price * qty
from dataclasses import dataclass, field
from datetime import datetime
from typing import List
from uuid import UUID, uuid4
@dataclass
class Money:
"""Value Object — immutable, equality by value"""
amount: int
currency: str
def __add__(self, other: 'Money') -> 'Money':
assert self.currency == other.currency
return Money(self.amount + other.amount, self.currency)
def __mul__() -> :
Money(.amount * qty, .currency)
:
product_id: UUID
product_name:
unit_price: Money
quantity:
() -> Money:
.unit_price * .quantity
:
MAX_ITEMS =
():
._ = order_id
._customer_id = customer_id
._lines: [OrderLine] = []
._status =
._events = []
() -> UUID:
._
() -> Money:
._lines:
Money(, )
((line.line_total line ._lines[:]),
._lines[].line_total)
() -> :
._status != :
ValueError()
(._lines) >= .MAX_ITEMS:
ValueError()
qty <= :
ValueError()
line ._lines:
line.product_id == product_id:
._lines.remove(line)
._lines.append(OrderLine(
product_id, name, price, line.quantity + qty))
._lines.append(OrderLine(product_id, name, price, qty))
() -> :
._lines:
ValueError()
._status != :
ValueError()
._status =
._events.append(OrderConfirmed(
order_id=._,
customer_id=._customer_id,
total=.total,
occurred_at=datetime.utcnow()
))
() -> :
events = (._events)
._events.clear()
events
:
order_id: UUID
customer_id: UUID
total: Money
occurred_at: datetime
Value Objects
@dataclass(frozen=True)
class EmailAddress:
value: str
def __post_init__(self):
import re
if not re.match(r'^[^@]+@[^@]+\.[^@]+$', self.value):
raise ValueError(f"Invalid email: {self.value}")
@dataclass(frozen=True)
class PostalAddress:
street: str
city: str
country_code: str
postal_code: str
def is_domestic(self, country: str) -> bool:
return self.country_code == country
class Customer:
def __init__(self, id: UUID, email: EmailAddress):
self._id = id
self._email = email
def change_email() -> :
._email == new_email:
ValueError()
._email = new_email
._events.append(CustomerEmailChanged(._, new_email))
Repository Pattern
from abc import ABC, abstractmethod
class OrderRepository(ABC):
"""Interface in domain layer — implementation in infrastructure"""
@abstractmethod
def get(self, order_id: UUID) -> Order:
"""Raise OrderNotFound if not present"""
...
@abstractmethod
def save(self, order: Order) -> None:
"""Persist aggregate and publish domain events"""
...
@abstractmethod
def next_id(self) -> UUID:
"""Generate next aggregate ID"""
...
class PostgresOrderRepository(OrderRepository):
def __init__(self, session, event_publisher):
self._session = session
self._event_publisher = event_publisher
def get(self, order_id: UUID) -> Order:
record = self._session.query(OrderRecord).get(order_id)
if not record:
raise OrderNotFound(order_id)
return self._reconstruct(record)
def save(self, order: Order) -> :
record = ._to_record(order)
._session.merge(record)
._session.flush()
events = order.pull_events()
event events:
._event_publisher.publish(event)
() -> UUID:
uuid4()
() -> Order:
...
():
...
Domain Services
class PricingService:
"""Domain Service — stateless, operates on multiple aggregates"""
def __init__(self, discount_repo: DiscountRepository):
self._discounts = discount_repo
def calculate_order_price(
self,
order: Order,
customer: Customer
) -> Money:
"""Pricing logic that spans Order and Customer"""
base = order.total
discounts = self._discounts.find_applicable(
customer_tier=customer.tier,
order_value=base
)
total_discount = sum(d.amount for d in discounts)
return Money(
max(0, base.amount - total_discount),
base.currency
)
Ubiquitous Language Glossary Template
# [Domain] Ubiquitous Language — [Context Name]
Last updated: YYYY-MM-DD
Context: [Bounded Context Name]
## Core Terms
| Term | Definition | NOT the same as | Used in code as |
|------|-----------|-----------------|-----------------|
| Order | A customer's intent to purchase items, from draft through fulfilment | Shopping Cart (pre-order intent) | `Order` class |
| Confirmation | The act of a customer committing to purchase, creating an obligation | Payment (separate step) | `order.confirm()` |
| Line Item | A single product+quantity combination within an order | Product (catalogue concept) | `OrderLine` |
| Fulfilment | The warehouse process of picking, packing, and shipping | Delivery (carrier action) | `FulfilmentRequest` |
## State Transitions
Draft → Confirmed → Fulfilling → Shipped → Delivered
↘ Cancelled
## Events
| Event | Meaning | Triggers |
|-------|---------|---------|
| OrderConfirmed | Customer committed to purchase | → Payment processing, → Inventory reservation |
| PaymentCaptured | Money collected | → Fulfilment request |
| OrderCancelled | Order voided | → Inventory release, → Refund if paid |
Worked Example: SaaS Subscription Platform
Domain: B2B SaaS with subscriptions, seats, billing, and access control.
Event Storm findings:
- Events: SubscriptionStarted, SeatAdded, SeatRevoked, InvoiceGenerated, PaymentFailed, SubscriptionCancelled, FeatureEnabled
- Clusters naturally form around: Subscription management, Billing, Access control, Notifications
Bounded Contexts:
┌──────────────────────┐ Published Language ┌─────────────────────┐
│ Subscription Context│─────────────────────────▶│ Billing Context │
│ │ (SubscriptionStarted, │ │
│ Aggregate: Account │ SeatChanged events) │ Aggregate: Invoice │
│ Aggregate: Plan │ │ Aggregate: Payment │
└──────────┬───────────┘ └─────────────────────┘
│
│ ACL (translates to internal permission model)
▼
┌──────────────────────┐
│ Access Context │
│ │
│ Aggregate: Seat │
│ Aggregate: Feature │
└──────────────────────┘
Key design decisions:
- "Account" in Subscription context = billing entity; "Account" in Access context = login credential. Different things — separate models, no shared class.
- Seat count enforced in Subscription aggregate (not Access). Access context subscribes to SeatRevoked events to disable login.
- Billing context is Conformist to Stripe's model for payment primitives.
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Fix |
|---|
| Anemic Domain Model | Logic lives in services, entities are data bags | Move behaviour into aggregates |
| Shared Database across contexts | Tight coupling, impossible to evolve independently | One DB schema per context minimum |
| God Aggregate | One aggregate owns half the domain | Break on consistency boundaries, not convenience |
| Leaking domain models | Passing Order aggregate to external API controller | Map to DTOs at the boundary |
| Skipping Ubiquitous Language | Devs use technical names, experts use business terms | Rename code to match expert language |
| Event Sourcing by default | Adds complexity without clear benefit | Only use ES when audit trail or temporal queries required |
| Too many bounded contexts | Micro-contexts with single aggregates, integration overhead | Contexts should be team-sized, not class-sized |
| Ignoring Conway's Law | Context boundaries don't match team boundaries | Align contexts with team ownership |
10 Rules
- Domain experts define the language — developers adopt it, not vice versa.
- One Ubiquitous Language per bounded context. Different contexts may reuse words with different meanings.
- Aggregates enforce invariants — if you can break a business rule without going through the root, the boundary is wrong.
- Keep aggregates small. If an aggregate has more than 4–6 entities, break it down.
- Reference other aggregates by ID only — never hold a direct object reference across aggregate roots.
- Domain events are facts in past tense:
OrderConfirmed, not ConfirmOrder.
- Repositories return complete aggregates, not partial projections.
- Domain Services are stateless. If they need state, they're probably an aggregate.
- Anti-Corruption Layers translate foreign models at the boundary — never pollute the domain layer.
- Refactor toward deeper insight. The first model is wrong. Continuous refinement is the method.