Domain-Driven Design: Strategic Patterns
Domain-Driven Design (DDD) helps model complex business domains. After applying DDD in production, here’s how to use strategic patterns effectively.
What is Domain-Driven Design?
DDD is:
- Domain-focused - Model reflects business
- Ubiquitous language - Shared vocabulary
- Bounded contexts - Explicit boundaries
- Strategic patterns - Context relationships
Bounded Contexts
Definition
A bounded context:
- Has explicit boundaries
- Contains domain model
- Uses ubiquitous language
- Owns data model
Example: E-Commerce
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ Catalog Context │ │ Order Context │ │ Shipping Context │
│ │ │ │ │ │
│ Product │ │ Order │ │ Shipment │
│ Category │ │ OrderItem │ │ DeliveryAddress │
│ Price │ │ Payment │ │ Tracking │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
Implementation
// Catalog Context
class Product {
constructor(id, name, price, category) {
this.id = id;
this.name = name;
this.price = price;
this.category = category;
}
updatePrice(newPrice) {
if (newPrice <= 0) {
throw new Error('Price must be positive');
}
this.price = newPrice;
}
}
// Order Context
class Order {
constructor(id, customerId, items) {
this.id = id;
this.customerId = customerId;
this.items = items;
this.status = 'pending';
}
addItem(productId, quantity, price) {
this.items.push({
productId,
quantity,
price
});
}
calculateTotal() {
return this.items.reduce((sum, item) =>
sum + (item.quantity * item.price), 0
);
}
}
Context Mapping
Patterns
Customer-Supplier:
- Upstream provides to downstream
- Downstream depends on upstream
- Upstream prioritizes downstream needs
Conformist:
- Downstream conforms to upstream
- No influence on upstream
- Accept upstream model as-is
Anti-Corruption Layer:
- Translates between contexts
- Protects domain model
- Isolates external systems
Shared Kernel:
- Shared code between contexts
- Requires coordination
- Use sparingly
Published Language:
- Well-documented interface
- Stable contract
- Used for integration
Context Map Example
┌──────────────┐
│ Catalog │ (Upstream)
└──────┬───────┘
│ Customer-Supplier
│
┌──────▼───────┐
│ Order │ (Downstream)
└──────┬───────┘
│ Anti-Corruption Layer
│
┌──────▼───────┐
│ Shipping │ (External)
└──────────────┘
Anti-Corruption Layer
Implementation
// External Shipping Service
class ExternalShippingService {
async createShipment(orderData) {
// External API format
return await fetch('https://shipping-api.com/shipments', {
method: 'POST',
body: JSON.stringify({
order_id: orderData.orderId,
customer_name: orderData.customerName,
address: orderData.address
})
});
}
}
// Anti-Corruption Layer
class ShippingAdapter {
constructor(externalService) {
this.externalService = externalService;
}
async createShipment(order) {
// Translate from domain model to external format
const externalData = {
orderId: order.id.value,
customerName: order.customer.name,
address: this.mapAddress(order.shippingAddress)
};
const result = await this.externalService.createShipment(externalData);
// Translate back to domain model
return this.mapToShipment(result);
}
mapAddress(address) {
return {
street: address.street,
city: address.city,
zip: address.postalCode
};
}
mapToShipment(externalResult) {
return new Shipment(
externalResult.shipment_id,
externalResult.tracking_number
);
}
}
Shared Kernel
When to Use
Use when:
- Tightly coupled contexts
- Shared domain concepts
- Requires coordination
Avoid when:
- Contexts can evolve independently
- Different teams
- Different release cycles
Implementation
// Shared Kernel
// shared-kernel/value-objects.js
class Money {
constructor(amount, currency) {
if (amount < 0) {
throw new Error('Amount cannot be negative');
}
this.amount = amount;
this.currency = currency;
}
add(other) {
if (this.currency !== other.currency) {
throw new Error('Cannot add different currencies');
}
return new Money(this.amount + other.amount, this.currency);
}
}
// Catalog Context uses shared kernel
import { Money } from '../shared-kernel/value-objects';
class Product {
constructor(id, name, price) {
this.id = id;
this.name = name;
this.price = price; // Money object
}
}
// Order Context uses shared kernel
import { Money } from '../shared-kernel/value-objects';
class Order {
calculateTotal() {
return this.items.reduce((total, item) =>
total.add(item.subtotal),
new Money(0, 'USD')
);
}
}
Published Language
Definition
Published language:
- Well-documented interface
- Stable contract
- Versioned
- Used for integration
Example
// Published Language - Order Events
class OrderCreatedEvent {
constructor(orderId, customerId, items, total) {
this.eventType = 'OrderCreated';
this.version = '1.0';
this.orderId = orderId;
this.customerId = customerId;
this.items = items.map(item => ({
productId: item.productId,
quantity: item.quantity,
price: item.price
}));
this.total = total;
this.timestamp = new Date().toISOString();
}
}
// Publisher
class OrderService {
async createOrder(orderData) {
const order = new Order(orderData);
await this.orderRepository.save(order);
// Publish event using published language
const event = new OrderCreatedEvent(
order.id,
order.customerId,
order.items,
order.total
);
await this.eventBus.publish(event);
return order;
}
}
// Subscriber (different bounded context)
class InventoryService {
async handleOrderCreated(event) {
// Consume published language
for (const item of event.items) {
await this.decreaseStock(item.productId, item.quantity);
}
}
}
Context Integration
Event-Driven Integration
// Order Context publishes events
class OrderService {
async shipOrder(orderId) {
const order = await this.orderRepository.findById(orderId);
order.markAsShipped();
await this.orderRepository.save(order);
// Publish event
await this.eventBus.publish({
type: 'OrderShipped',
orderId: order.id,
shippingAddress: order.shippingAddress
});
}
}
// Shipping Context subscribes to events
class ShippingService {
constructor(eventBus) {
this.eventBus = eventBus;
this.eventBus.subscribe('OrderShipped', this.handleOrderShipped.bind(this));
}
async handleOrderShipped(event) {
const shipment = await this.createShipment({
orderId: event.orderId,
address: event.shippingAddress
});
// Publish back
await this.eventBus.publish({
type: 'ShipmentCreated',
orderId: event.orderId,
trackingNumber: shipment.trackingNumber
});
}
}
Best Practices
- Identify bounded contexts - Based on domain
- Use ubiquitous language - Shared vocabulary
- Map relationships - Context mapping
- Protect boundaries - Anti-corruption layers
- Minimize shared kernel - Use sparingly
- Version published language - Stable contracts
- Document boundaries - Clear ownership
- Evolve independently - Minimize coupling
Common Mistakes
- Too many bounded contexts - Over-engineering
- Shared kernel everywhere - Tight coupling
- No context mapping - Unclear relationships
- Ignoring boundaries - Leaky abstractions
- No ubiquitous language - Miscommunication
Conclusion
Strategic DDD patterns enable:
- Clear boundaries
- Independent evolution
- Better integration
- Domain focus
Start with bounded contexts, then map relationships. The patterns shown here handle complex domains.
Domain-Driven Design strategic patterns from January 2020, covering bounded contexts and context mapping.