What is Domain-Driven Design (DDD) and what are its core principles?
Quick Answer
Domain-Driven Design is an approach to building software whose model deeply reflects the business domain, developed through close collaboration between developers and domain experts. Core principles include a Ubiquitous Language shared by all, Bounded Contexts that delimit each model's scope, and a focus on the core domain. It distinguishes strategic design (contexts, boundaries) from tactical patterns (entities, value objects, aggregates).
Detailed Answer
Domain-Driven Design (DDD) is a software development approach that focuses on creating software that reflects a deep understanding of the business domain. It emphasizes collaboration between technical and domain experts to build software that accurately models the business.
Core Principles of DDD:
-
Focus on the Domain
- The domain is the heart of the software
- Business logic should be the primary concern
- Technical concerns are secondary
-
Ubiquitous Language
- Use the same language throughout the codebase, documentation, and conversations
- Terms should be consistent between developers and domain experts
- Code should reflect business terminology
-
Model-Driven Design
- The code should be a direct reflection of the domain model
- Changes in understanding should lead to changes in the code
- The model should evolve with business understanding
Example of Ubiquitous Language:
// ❌ Technical language
public class UserAccount
{
public int Id { get; set; }
public string Username { get; set; }
public bool IsActive { get; set; }
}
// ✅ Domain language
public class Customer
{
public CustomerId Id { get; set; }
public CustomerName Name { get; set; }
public CustomerStatus Status { get; set; }
}
public enum CustomerStatus
{
Active,
Suspended,
Closed
}
Strategic Design Patterns:
- Bounded Contexts
- Define clear boundaries around models
- Each context has its own ubiquitous language
- Models can be different in different contexts
// E-commerce context
public class Product
{
public ProductId Id { get; set; }
public ProductName Name { get; set; }
public Money Price { get; set; }
public ProductCategory Category { get; set; }
}
// Inventory context
public class InventoryItem
{
public InventoryItemId Id { get; set; }
public string SKU { get; set; }
public int QuantityOnHand { get; set; }
public int ReorderLevel { get; set; }
}
- Context Mapping
- Define relationships between bounded contexts
- Shared Kernel, Customer-Supplier, Conformist, Anti-Corruption Layer
Tactical Design Patterns:
- Entities
- Objects with identity that persists over time
- Identity is more important than attributes
public class Order : Entity<OrderId>
{
public OrderId Id { get; private set; }
public CustomerId CustomerId { get; private set; }
public OrderStatus Status { get; private set; }
private readonly List<OrderItem> _items = new();
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
public void AddItem(ProductId productId, Quantity quantity, Money unitPrice)
{
// Business logic for adding items
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot add items to a confirmed order");
_items.Add(new OrderItem(productId, quantity, unitPrice));
}
}
- Value Objects
- Objects defined by their attributes, not identity
- Immutable and comparable by value
public class Money : ValueObject
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
if (amount < 0) throw new ArgumentException("Amount cannot be negative");
if (string.IsNullOrEmpty(currency)) throw new ArgumentException("Currency is required");
Amount = amount;
Currency = currency;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
public static Money operator +(Money left, Money right)
{
if (left.Currency != right.Currency)
throw new InvalidOperationException("Cannot add different currencies");
return new Money(left.Amount + right.Amount, left.Currency);
}
}
- Aggregates
- Cluster of related objects treated as a unit
- One aggregate root controls access to the cluster
public class Order : AggregateRoot<OrderId>
{
private readonly List<OrderItem> _items = new();
public void Confirm()
{
if (_items.Count == 0)
throw new InvalidOperationException("Cannot confirm an empty order");
Status = OrderStatus.Confirmed;
AddDomainEvent(new OrderConfirmedEvent(Id, CustomerId));
}
public void Cancel()
{
if (Status == OrderStatus.Shipped)
throw new InvalidOperationException("Cannot cancel a shipped order");
Status = OrderStatus.Cancelled;
AddDomainEvent(new OrderCancelledEvent(Id, CustomerId));
}
}
- Domain Services
- Operations that don't naturally belong to entities or value objects
- Stateless operations that involve multiple domain objects
public class OrderPricingService : IDomainService
{
public Money CalculateTotal(Order order, ICustomerRepository customerRepository)
{
var customer = customerRepository.GetById(order.CustomerId);
var baseTotal = order.Items.Sum(item => item.Total);
// Apply customer-specific discounts
var discount = customer.GetDiscountPercentage();
var discountAmount = baseTotal * (discount / 100);
return baseTotal - discountAmount;
}
}
- Domain Events
- Something important that happened in the domain
- Used for decoupling and integration
public class OrderConfirmedEvent : DomainEvent
{
public OrderId OrderId { get; }
public CustomerId CustomerId { get; }
public DateTime ConfirmedAt { get; }
public OrderConfirmedEvent(OrderId orderId, CustomerId customerId)
{
OrderId = orderId;
CustomerId = customerId;
ConfirmedAt = DateTime.UtcNow;
}
}
// Event handler
public class OrderConfirmedEventHandler : IDomainEventHandler<OrderConfirmedEvent>
{
private readonly IEmailService _emailService;
public async Task Handle(OrderConfirmedEvent domainEvent)
{
await _emailService.SendOrderConfirmationAsync(domainEvent.CustomerId, domainEvent.OrderId);
}
}
Benefits of DDD:
- Better Communication: Ubiquitous language improves team communication
- Focused Design: Clear boundaries prevent complexity
- Business Alignment: Software reflects business understanding
- Maintainability: Well-structured domain models are easier to maintain
- Testability: Clear domain logic is easier to test
When to Use DDD:
- Complex business domains
- Long-lived applications
- When business logic is the primary concern
- When you have access to domain experts
- When the domain is well-understood
Challenges of DDD:
- Requires domain expertise
- Can be overkill for simple applications
- Initial learning curve
- Requires discipline to maintain boundaries
- Can lead to over-engineering if not applied judiciously