domain-driven-design
npx skills add https://github.com/rsmdt/the-startup --skill domain-driven-design
Agent 安装分布
Skill 文档
Domain-Driven Design Patterns
Patterns for modeling complex business domains with clear boundaries, enforced invariants, and appropriate consistency strategies.
When to Activate
- Modeling business domains and entities
- Designing aggregate boundaries
- Implementing complex business rules
- Planning data consistency strategies
- Establishing bounded contexts
- Designing domain events and integration
Strategic Patterns
Bounded Context
A bounded context defines the boundary within which a domain model applies. The same term can mean different things in different contexts.
Example: "Customer" in different contexts
âââââââââââââââââââ âââââââââââââââââââ âââââââââââââââââââ
â Sales â â Support â â Billing â
â Context â â Context â â Context â
ââââââââââââââââââ⤠ââââââââââââââââââ⤠âââââââââââââââââââ¤
â Customer: â â Customer: â â Customer: â
â - Leads â â - Tickets â â - Invoices â
â - Opportunities â â - SLA â â - Payment â
â - Proposals â â - Satisfaction â â - Credit Limit â
âââââââââââââââââââ âââââââââââââââââââ âââââââââââââââââââ
Context Identification
Ask these questions to find context boundaries:
- Where does the ubiquitous language change?
- Which teams own which concepts?
- Where do integration points naturally occur?
- What could be deployed independently?
Context Mapping
Define how bounded contexts integrate:
| Pattern | Description | Use When |
|---|---|---|
| Shared Kernel | Shared code between contexts | Close collaboration, same team |
| Customer-Supplier | Upstream/downstream relationship | Clear dependency direction |
| Conformist | Downstream adopts upstream model | No negotiation power |
| Anti-Corruption Layer | Translation layer between models | Protecting domain from external models |
| Open Host Service | Published API for integration | Multiple consumers |
| Published Language | Shared interchange format | Industry standards exist |
Ubiquitous Language
The shared vocabulary between developers and domain experts:
Building Ubiquitous Language:
1. EXTRACT terms from domain expert conversations
2. DOCUMENT in a glossary with precise definitions
3. ENFORCE in code - class names, method names, variables
4. EVOLVE as understanding deepens
Example Glossary Entry:
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â Term: Order â
â Definition: A confirmed request from a customer to purchase â
â one or more products at agreed prices. â
â NOT: A shopping cart (which is an Intent, not an Order) â
â Context: Sales â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Tactical Patterns
Entities
Objects with identity that persists over time. Equality is based on identity, not attributes.
Characteristics:
- Has a unique identifier
- Mutable state
- Lifecycle (created, modified, archived)
- Equality by ID
Example:
âââââââââââââââââââââââââââââââââââââââââââ
â Entity: Order â
âââââââââââââââââââââââââââââââââââââââââââ¤
â Identity: orderId (UUID) â
â State: status, items, total â
â Behavior: addItem(), submit(), cancel() â
âââââââââââââââââââââââââââââââââââââââââââ
class Order {
private readonly id: OrderId; // Identity - immutable
private status: OrderStatus; // State - mutable
private items: OrderItem[]; // State - mutable
constructor(id: OrderId) {
this.id = id;
this.status = OrderStatus.Draft;
this.items = [];
}
equals(other: Order): boolean {
return this.id.equals(other.id); // Equality by identity
}
}
Value Objects
Objects without identity. Equality is based on attributes. Always immutable.
Characteristics:
- No unique identifier
- Immutable (all properties readonly)
- Equality by attributes
- Self-validating
Example:
âââââââââââââââââââââââââââââââââââââââââââ
â Value Object: Money â
âââââââââââââââââââââââââââââââââââââââââââ¤
â Attributes: amount, currency â
â Behavior: add(), subtract(), format() â
â Invariant: amount >= 0 â
âââââââââââââââââââââââââââââââââââââââââââ
class Money {
constructor(
public readonly amount: number,
public readonly currency: Currency
) {
if (amount < 0) throw new Error('Amount cannot be negative');
}
add(other: Money): Money {
if (!this.currency.equals(other.currency)) {
throw new Error('Cannot add different currencies');
}
return new Money(this.amount + other.amount, this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount &&
this.currency.equals(other.currency);
}
}
When to Use Value Objects
| Use Value Object | Use Entity |
|---|---|
| No need to track over time | Need to track lifecycle |
| Interchangeable instances | Unique identity matters |
| Defined by attributes | Defined by continuity |
| Examples: Money, Address, DateRange | Examples: User, Order, Account |
Aggregates
A cluster of entities and value objects with a defined boundary. One entity is the aggregate root.
Aggregate Design Rules:
1. PROTECT invariants at aggregate boundary
2. REFERENCE other aggregates by identity only
3. UPDATE one aggregate per transaction
4. DESIGN small aggregates (prefer single entity)
Example:
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â Aggregate: Order â
â Root: Order (entity) â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ¤
â âââââââââââââââââââ â
â â Order (Root) ââââ Aggregate Root â
â â - orderId â â
â â - customerId ââââ¼ââ⺠Reference by ID only â
â â - status â â
â ââââââââââ¬âââââââââ â
â â â
â ââââââââââ¼âââââââââ â
â â OrderItem ââââ Inside aggregate â
â â - productId âââââ¼ââ⺠Reference by ID only â
â â - quantity â â
â â - price (Money) ââââ Value Object â
â âââââââââââââââââââ â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Aggregate Sizing
Start Small:
- Begin with single-entity aggregates
- Expand only when invariants require it
Signs of Too-Large Aggregate:
- Frequent optimistic lock conflicts
- Loading too much data for simple operations
- Multiple users editing simultaneously
- Transactional failures across unrelated data
Signs of Too-Small Aggregate:
- Invariants not protected
- Business rules scattered across services
- Eventual consistency where immediate is required
Domain Events
Represent something that happened in the domain. Immutable facts about the past.
Event Structure:
âââââââââââââââââââââââââââââââââââââââââââ
â Event: OrderPlaced â
âââââââââââââââââââââââââââââââââââââââââââ¤
â eventId: UUID â
â occurredAt: DateTime â
â aggregateId: orderId â
â payload: â
â - customerId â
â - items â
â - totalAmount â
âââââââââââââââââââââââââââââââââââââââââââ
Naming Convention:
- Past tense (OrderPlaced, not PlaceOrder)
- Domain language (not technical)
- Include all relevant data (event is immutable)
class OrderPlaced implements DomainEvent {
readonly eventId = uuid();
readonly occurredAt = new Date();
constructor(
readonly orderId: OrderId,
readonly customerId: CustomerId,
readonly items: OrderItemData[],
readonly totalAmount: Money
) {}
}
Event Patterns
| Pattern | Description | Use Case |
|---|---|---|
| Event Notification | Minimal data, query for details | Loose coupling |
| Event-Carried State | Full data in event | Performance, offline |
| Event Sourcing | Events as source of truth | Audit, temporal queries |
Repositories
Abstract persistence, providing collection-like access to aggregates.
Repository Principles:
- One repository per aggregate
- Returns aggregate roots only
- Hides persistence mechanism
- Supports aggregate reconstitution
interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
findByCustomer(customerId: CustomerId): Promise<Order[]>;
save(order: Order): Promise<void>;
delete(order: Order): Promise<void>;
}
// Implementation hides persistence details
class PostgresOrderRepository implements OrderRepository {
async findById(id: OrderId): Promise<Order | null> {
const row = await this.db.query('SELECT * FROM orders WHERE id = $1', [id]);
return row ? this.reconstitute(row) : null;
}
private reconstitute(row: OrderRow): Order {
// Rebuild aggregate from persistence
}
}
Consistency Strategies
Transactional Consistency (ACID)
Use for invariants within an aggregate:
Rule: One aggregate per transaction
// Good: Single aggregate updated
async function addItemToOrder(orderId: OrderId, item: OrderItem) {
const order = await orderRepo.findById(orderId);
order.addItem(item); // Business rules enforced
await orderRepo.save(order);
}
// Bad: Multiple aggregates in one transaction
async function createOrderWithInventory() {
await db.transaction(async (tx) => {
await orderRepo.save(order, tx);
await inventoryRepo.decrement(productId, quantity, tx); // Don't do this
});
}
Eventual Consistency
Use for consistency across aggregates:
Pattern: Domain Events + Handlers
// Order aggregate publishes event
class Order {
submit(): void {
this.status = OrderStatus.Placed;
this.addEvent(new OrderPlaced(this.id, this.customerId, this.items));
}
}
// Separate handler updates inventory (eventually)
class InventoryHandler {
async handle(event: OrderPlaced): Promise<void> {
for (const item of event.items) {
await this.inventoryService.reserve(item.productId, item.quantity);
}
}
}
Saga Pattern
Coordinate multiple aggregates with compensation:
Saga: Order Fulfillment
âââââââââââ âââââââââââââââ âââââââââââââââ âââââââââââ
â Create ââââââºâ Reserve ââââââºâ Charge ââââââºâ Ship â
â Order â â Inventory â â Payment â â Order â
ââââââ¬âââââ ââââââââ¬âââââââ ââââââââ¬âââââââ âââââââââââ
â â â
â Compensate: â Compensate: â Compensate:
â Cancel Order â Release Inventory â Refund Payment
â¼ â¼ â¼
On failure at any step, execute compensation in reverse order.
Choosing Consistency
| Scenario | Strategy |
|---|---|
| Within single aggregate | Transactional (ACID) |
| Across aggregates, same service | Eventual (domain events) |
| Across services | Saga with compensation |
| Read model updates | Eventual (projection) |
Anti-Patterns
Anemic Domain Model
// Anti-pattern: Logic outside domain objects
class Order {
id: string;
items: Item[];
status: string;
}
class OrderService {
calculateTotal(order: Order): number { ... }
validate(order: Order): boolean { ... }
submit(order: Order): void { ... }
}
// Better: Logic inside domain objects
class Order {
private items: OrderItem[];
private status: OrderStatus;
get total(): Money {
return this.items.reduce((sum, item) => sum.add(item.subtotal), Money.zero());
}
submit(): void {
this.validate();
this.status = OrderStatus.Submitted;
}
}
Large Aggregates
// Anti-pattern: Everything in one aggregate
class Customer {
orders: Order[]; // Could be thousands
addresses: Address[];
paymentMethods: PaymentMethod[];
preferences: Preferences;
activityLog: Activity[]; // Could be millions
}
// Better: Separate aggregates referenced by ID
class Customer {
id: CustomerId;
defaultAddressId: AddressId;
defaultPaymentMethodId: PaymentMethodId;
}
class Order {
customerId: CustomerId; // Reference by ID
}
Primitive Obsession
// Anti-pattern: Primitive types for domain concepts
function createOrder(
customerId: string,
productId: string,
quantity: number,
price: number,
currency: string
) { ... }
// Better: Value objects
function createOrder(
customerId: CustomerId,
productId: ProductId,
quantity: Quantity,
price: Money
) { ... }
Implementation Checklist
Aggregate Design
- Single entity can be aggregate root
- Invariants are protected at boundary
- Other aggregates referenced by ID only
- Fits in memory comfortably
- One transaction per aggregate
Entity Implementation
- Has unique identifier
- Equality based on ID
- Encapsulates business rules
- State changes through methods
Value Object Implementation
- All properties immutable
- Equality based on attributes
- Self-validating
- Operations return new instances
Repository Implementation
- One per aggregate
- Returns aggregate roots only
- Hides persistence details
- Supports queries needed by domain
References
- Pattern Implementation Examples – Code examples in multiple languages
- Aggregate Design Guide – Detailed aggregate sizing heuristics