Domain-Driven Design (DDD)
Overview
Domain-Driven Design (DDD) là một approach trong software development tập trung vào việc model hóa business domain. Thay vì tập trung vào database hay UI, DDD đặt business logic và domain models làm trung tâm của ứng dụng.
Core Concepts
1. Ubiquitous Language
Ngôn ngữ chung được sử dụng bởi tất cả team members (developers, domain experts, business users) để mô tả domain.
// Thay vì "Order" hay "SalesOrder"
// Sử dụng một tên nhất quán trong code, documentation, và conversations
public class PurchaseOrder // Domain language
{
// Không phải "CustomerId" mà là "BuyerId" nếu business gọi là "buyer"
public Buyer Buyer { get; }
public List<LineItem> LineItems { get; }
public OrderStatus Status { get; }
public void Submit() { } // Không phải "Place" hay "Create"
public void Confirm() { } // Không phải "Approve"
}
2. Bounded Context
Mỗi domain model có một boundary rõ ràng nơi nó có nghĩa. Different contexts có thể có different models for the same concept.
┌─────────────────┐ ┌─────────────────┐
│ Order Context │ │ Shipping │
│ │ │ Context │
│ - Order │ │ - Shipment │
│ - Customer │ │ - Delivery │
│ - Payment │ │ - Address │
└─────────────────┘ └─────────────────┘
│ │
│ Shared │
│ Language │
▼ ▼
(Giữ riêng biệt, có integration point)
Building Blocks
1. Entities
Objects có identity riêng biệt, không chỉ dựa trên attributes.
public class Order : Entity
{
public OrderId Id { get; private set; } // Identity
public Customer Customer { get; private set; }
public List<OrderItem> Items { get; private set; }
public OrderStatus Status { get; private set; }
public DateTime CreatedAt { get; private set; }
// Identity matters - two orders with same data are still different
// e.g., Order #1001 vs Order #1002
public void AddItem(Product product, int quantity)
{
// Business logic
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot add items to placed order");
Items.Add(new OrderItem(product, quantity));
}
public void Place()
{
if (!Items.Any())
throw new InvalidOperationException("Cannot place empty order");
Status = OrderStatus.Placed;
AddDomainEvent(new OrderPlacedEvent(this));
}
}
2. Value Objects
Objects không có identity, chỉ defined by their attributes.
// Two addresses with same values are considered equal
public class Address : ValueObject
{
public string Street { get; }
public string City { get; }
public string State { get; }
public string ZipCode { get; }
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Street;
yield return City;
yield return State;
yield return ZipCode;
}
}
// Money - another common value object
public class Money : ValueObject
{
public decimal Amount { get; }
public Currency Currency { get; }
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Cannot add different currencies");
return new Money(Amount + other.Amount, Currency);
}
}
3. Aggregates
Group of related entities and value objects treated as a single unit.
// OrderAggregate - root entity
public class Order : AggregateRoot
{
private readonly List<OrderItem> _items = new();
public OrderId Id { get; private set; }
public CustomerId CustomerId { get; private set; }
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
public void AddItem(Product product, int quantity)
{
// All invariants enforced here
var existingItem = _items.FirstOrDefault(i => i.ProductId == product.Id);
if (existingItem != null)
{
existingItem.IncreaseQuantity(quantity);
}
else
{
_items.Add(new OrderItem(product, quantity));
}
}
// Only OrderAggregate can create OrderItem
// External code cannot bypass invariants
}
public class OrderItem
{
public OrderItemId Id { get; private set; }
public ProductId ProductId { get; private set; }
public int Quantity { get; private set; }
public Money UnitPrice { get; private set; }
internal OrderItem(Product product, int quantity) // Internal constructor
{
// ...
}
internal void IncreaseQuantity(int quantity)
{
Quantity += quantity;
}
}
4. Domain Services
Business logic không thuộc về Entity hoặc Value Object.
public class PricingService
{
public Money CalculateOrderTotal(Order order, Discount discount)
{
var subtotal = order.Items
.Sum(i => i.UnitPrice.Amount * i.Quantity);
var discountAmount = subtotal * discount.Percentage;
return new Money(subtotal - discountAmount, Currency.USD);
}
}
public class InventoryService
{
public bool IsAvailable(ProductId productId, int quantity)
{
// Cross-aggregate business logic
var product = _productRepository.GetById(productId);
var reserved = _reservationRepository.GetReservedQuantity(productId);
return product.StockQuantity - reserved >= quantity;
}
}
5. Repositories
Abstraction cho việc access domain objects.
public interface IOrderRepository
{
Order GetById(OrderId id);
Task<Order> GetByIdAsync(OrderId id);
void Add(Order order);
void Update(Order order);
// Not Delete - domain determines when to "archive"
}
// Infrastructure implementation
public class EfOrderRepository : IOrderRepository
{
private readonly DbContext _context;
public Order GetById(OrderId id)
{
return _context.Orders
.Include(o => o.Items)
.FirstOrDefault(o => o.Id == id);
}
public void Add(Order order)
{
_context.Orders.Add(order);
}
}
6. Domain Events
Events that represent something that happened in the domain.
public abstract class DomainEvent
{
public Guid Id { get; } = Guid.NewGuid();
public DateTime OccurredAt { get; } = DateTime.UtcNow;
}
public class OrderPlacedEvent : DomainEvent
{
public OrderId OrderId { get; }
public CustomerId CustomerId { get; }
public Money Total { get; }
public OrderPlacedEvent(Order order)
{
OrderId = order.Id;
CustomerId = order.CustomerId;
Total = order.Total;
}
}
// Event dispatching
public class Order : AggregateRoot
{
public void Place()
{
Status = OrderStatus.Placed;
// Add domain event
AddDomainEvent(new OrderPlacedEvent(this));
}
}
Strategic Patterns
1. Context Mapping
┌──────────────┐ ┌──────────────┐
│ Context A │ │ Context B │
│ (Core) │────▶│ (Supporting)│
│ │ API│ │
└──────────────┘ └──────────────┘
Types of relationships:
- Partnership: Teams work together
- Customer-Supplier: One serves another
- Conformist: Downstream conforms to upstream
- Anticorruption Layer: Protect from upstream changes
2. Anti-Corruption Layer
// Translating between contexts
public class CustomerAdapter
{
private readonly ExternalCustomerService _externalService;
public Domain.Customer GetCustomer(string externalId)
{
var externalCustomer = _externalService.GetById(externalId);
// Transform to domain model
return new Domain.Customer
{
Id = externalCustomer.Id,
Name = externalCustomer.FullName,
Email = new Email(externalCustomer.EmailAddress)
};
}
}
Implementation in .NET
// Base classes
public abstract class Entity
{
public Guid Id { get; protected set; }
public bool Equals(Entity other) => Id == other?.Id;
public override int GetHashCode() => Id.GetHashCode();
}
public abstract class ValueObject
{
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object obj)
{
if (obj is not ValueObject other) return false;
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
=> GetEqualityComponents().Aggregate(0, (h, c) => h ^ c.GetHashCode());
}
public abstract class AggregateRoot : Entity
{
private readonly List<DomainEvent> _domainEvents = new();
public IReadOnlyList<DomainEvents> DomainEvents => _domainEvents.AsReadOnly();
protected void AddDomainEvent(DomainEvent event)
{
_domainEvents.Add(event);
}
public void ClearDomainEvents() => _domainEvents.Clear();
}
Benefits of DDD
| Benefit | Description |
|---|---|
| Business Focus | Code mirrors business language |
| Complex Domains | Better handling of complex business logic |
| Communication | Improved team communication |
| Testability | Domain logic is easily testable |
| Flexibility | Easier to adapt to changing requirements |
When to Use DDD
- Complex business domains
- Large teams needing shared understanding
- Long-lived applications
- Domain-driven requirements
When NOT to Use DDD
- Simple CRUD applications
- Low business complexity
- Quick prototypes
- Small teams with simple needs
References
- Clean Architecture
- CQRS
- Event Sourcing
- Eric Evans - “Domain-Driven Design: Tackling Complexity in the Heart of Software”