Architecture
Domain-Driven Design
Align software with business domains
Advanced
architecture patterns modeling ddd
Definition
Domain-Driven Design (DDD) is an approach to software development that focuses on modeling the business domain. It aligns software design with the actual business requirements by establishing a common language between developers and domain experts, creating bounded contexts, and using strategic patterns to manage complexity.
Core Concepts
Ubiquitous Language
// ❌ Technical terms mixed with business
function processOrder(orderData) {
const dbRecord = convertToDBFormat(orderData);
const result = db.insert(dbRecord);
return result.id;
}
// ✅ Domain language only
function placeOrder(orderRequest) {
const order = Order.create(orderRequest);
orderRepository.save(order);
return order.orderId;
}
// Common language shared by team
class Order {
constructor(customer, items) {
this.customer = customer;
this.lineItems = items.map(item => new LineItem(item));
this.status = OrderStatus.PENDING;
this.placedAt = new Date();
}
confirm() {
if (this.status !== OrderStatus.PENDING) {
throw new Error('Only pending orders can be confirmed');
}
this.status = OrderStatus.CONFIRMED;
this.raiseEvent(new OrderConfirmed(this.orderId));
}
cancel(reason) {
if (this.status === OrderStatus.SHIPPED) {
throw new Error('Cannot cancel shipped order');
}
this.status = OrderStatus.CANCELLED;
this.cancellationReason = reason;
}
}
Bounded Contexts
┌─────────────────────────────────────────────────────────────┐
│ E-commerce System │
├───────────────────────┬─────────────────────────────────────┤
│ Sales Context │ Inventory Context │
│ │ │
│ Order │ Product │
│ Customer │ Stock │
│ Pricing │ Warehouse │
│ Promotion │ Supplier │
│ │ │
│ Language: │ Language: │
│ - "Place order" │ - "Reserve stock" │
│ - "Apply discount" │ - "Check availability" │
└───────────────────────┴─────────────────────────────────────┘
Different contexts may use same term differently:
- Sales: Product = catalog item with price
- Inventory: Product = physical item with stock level
Building Blocks
Entities
// Has identity that persists over time
class Customer {
constructor(id, name, email) {
this.customerId = id; // Identity
this.name = name;
this.email = email;
}
changeEmail(newEmail) {
// Validation logic
if (!isValidEmail(newEmail)) {
throw new Error('Invalid email');
}
this.email = newEmail;
this.raiseEvent(new CustomerEmailChanged(this.customerId, newEmail));
}
}
Value Objects
// Immutable, defined by attributes, no identity
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
Object.freeze(this);
}
add(other) {
if (this.currency !== other.currency) {
throw new Error('Cannot add different currencies');
}
return new Money(this.amount + other.amount, this.currency);
}
equals(other) {
return this.amount === other.amount &&
this.currency === other.currency;
}
}
// Usage
const price = new Money(100, 'USD');
const discount = new Money(10, 'USD');
const total = price.add(discount); // New instance
Aggregates
// Consistency boundary, root entity controls access
class Order extends AggregateRoot {
constructor(id, customerId) {
super(id);
this.customerId = customerId;
this.lineItems = [];
this.status = OrderStatus.PENDING;
}
addItem(product, quantity) {
if (this.status !== OrderStatus.PENDING) {
throw new Error('Cannot modify confirmed order');
}
const existingItem = this.lineItems.find(
item => item.productId === product.id
);
if (existingItem) {
existingItem.increaseQuantity(quantity);
} else {
this.lineItems.push(new LineItem(product, quantity));
}
this.recalculateTotal();
}
removeItem(productId) {
this.lineItems = this.lineItems.filter(
item => item.productId !== productId
);
this.recalculateTotal();
}
recalculateTotal() {
this.total = this.lineItems.reduce(
(sum, item) => sum.add(item.subtotal),
new Money(0, 'USD')
);
}
}
Domain Events
class OrderConfirmed {
constructor(orderId, confirmedAt, customerId) {
this.orderId = orderId;
this.confirmedAt = confirmedAt;
this.customerId = customerId;
}
}
// Publishing events
class Order {
confirm() {
this.status = OrderStatus.CONFIRMED;
this.addDomainEvent(new OrderConfirmed(
this.orderId,
new Date(),
this.customerId
));
}
}
// Handling events
class InventoryService {
handleOrderConfirmed(event) {
this.reserveStock(event.orderId);
}
}
class NotificationService {
handleOrderConfirmed(event) {
this.sendConfirmationEmail(event.customerId);
}
}
Strategic Design
Context Mapping
// Define relationships between bounded contexts
const contextMap = {
sales: {
relationships: {
inventory: {
type: 'customer-supplier',
upstream: 'inventory',
downstream: 'sales'
},
shipping: {
type: 'conformist',
upstream: 'shipping',
downstream: 'sales'
}
}
}
};
// Anti-corruption layer
class InventoryAdapter {
constructor(inventoryService) {
this.inventoryService = inventoryService;
}
async checkAvailability(productId, quantity) {
// Translate external model to our domain
const externalResult = await this.inventoryService.checkStock(productId);
return {
isAvailable: externalResult.stockLevel >= quantity,
reservedQuantity: quantity
};
}
}
Repository Pattern
// Abstract data access
class OrderRepository {
async findById(id) {
const data = await this.db.query('SELECT * FROM orders WHERE id = ?', [id]);
return this.mapToEntity(data);
}
async save(order) {
const events = order.domainEvents;
await this.db.transaction(async (trx) => {
await trx.insert('orders', this.mapToData(order));
// Persist events for outbox pattern
for (const event of events) {
await trx.insert('domain_events', {
type: event.constructor.name,
payload: JSON.stringify(event),
occurred_at: new Date()
});
}
});
order.clearEvents();
}
}
Application Services
// Orchestrate use cases
class OrderApplicationService {
constructor(orderRepository, inventoryService, paymentService) {
this.orderRepository = orderRepository;
this.inventoryService = inventoryService;
this.paymentService = paymentService;
}
async placeOrder(command) {
// 1. Check inventory
const availability = await this.inventoryService.checkAvailability(
command.productId,
command.quantity
);
if (!availability.isAvailable) {
throw new Error('Product out of stock');
}
// 2. Create order
const order = new Order(
generateId(),
command.customerId
);
order.addItem(
{ id: command.productId, price: command.price },
command.quantity
);
// 3. Process payment
const payment = await this.paymentService.charge(
command.paymentMethod,
order.total
);
if (payment.success) {
order.confirm();
}
// 4. Save
await this.orderRepository.save(order);
// Return DTO
return OrderDto.from(order);
}
}
Frontend DDD
Module Organization
src/
├── modules/
│ ├── sales/
│ │ ├── domain/
│ │ │ ├── order.ts
│ │ │ ├── customer.ts
│ │ │ └── events/
│ │ ├── application/
│ │ │ ├── order.service.ts
│ │ │ └── dto/
│ │ ├── infrastructure/
│ │ │ ├── order.repository.ts
│ │ │ └── api/
│ │ └── presentation/
│ │ ├── components/
│ │ └── hooks/
│ └── inventory/
│ └── ...
State Management
// Domain logic in store
const orderStore = createStore({
state: () => ({
order: null
}),
actions: {
addItem(product, quantity) {
// Domain logic here
if (this.order.status !== 'PENDING') {
throw new Error('Cannot modify order');
}
this.order.addItem(product, quantity);
},
confirm() {
this.order.confirm();
// Emit event
eventBus.emit('order:confirmed', this.order);
}
}
});
Key Takeaway
DDD aligns software with business by establishing ubiquitous language, creating bounded contexts, and using tactical patterns like entities, value objects, aggregates, and domain events. In frontend applications, focus on domain modeling in state management and clear module boundaries. DDD shines in complex domains where business logic is intricate.