--- name: domain-driven-design description: DDD patterns cho complex business logic. Use for Aggregates, Value Objects, Entities, Domain Events, và Rich Domain Model. compatibility: ".NET 8+, EF Core 8+" metadata: author: Velik Ho version: "1.0" --- # Domain-Driven Design Patterns / Mẫu DDD DDD patterns cho GoodGo microservices với complex business logic. ## When to Use This Skill / Khi Nào Sử Dụng Use this skill when: - Modeling complex business domains / Mô hình hóa domain phức tạp - Designing aggregates and entities / Thiết kế aggregates và entities - Implementing business rules in domain / Triển khai business rules trong domain - Creating value objects / Tạo value objects - Raising domain events / Raise domain events ## Core Concepts / Khái Niệm Cốt Lõi ### DDD Building Blocks / Các Khối Xây Dựng DDD ``` ┌─────────────────────────────────────────────────────────────┐ │ DOMAIN MODEL LAYER │ ├─────────────────────────────────────────────────────────────┤ │ ┌─────────────────────────────────────────────────────┐ │ │ │ AGGREGATE ROOT │ │ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ │ │ Entity │ │ Value Object │ │ │ │ │ │ (Identity) │ │ (No Identity) │ │ │ │ │ └─────────────────┘ └─────────────────┘ │ │ │ │ │ │ │ │ • Business Rules / Quy tắc nghiệp vụ │ │ │ │ • Domain Events / Domain Events │ │ │ │ • Invariants / Bất biến │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ Domain Service │ │ Domain Events │ │ │ └─────────────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ### Entity vs Value Object / Entity vs Value Object | Aspect | Entity | Value Object | |--------|--------|--------------| | **Identity** | Has unique ID | No identity | | **Equality** | By ID | By all properties | | **Mutability** | Mutable (via methods) | Immutable | | **Lifecycle** | Independent | Belongs to Entity | | **Example** | Order, User | Address, Money | ### Aggregate Rules / Quy Tắc Aggregate 1. **One repository per aggregate root** / Một repository cho mỗi aggregate root 2. **Reference only by ID** / Chỉ tham chiếu qua ID 3. **Atomic transaction boundary** / Ranh giới transaction atomic 4. **Consistency within aggregate** / Nhất quán trong aggregate ## Key Patterns / Mẫu Chính ### Entity Base Class ```csharp /// /// EN: Base class for all entities. /// VI: Base class cho tất cả entities. /// public abstract class Entity { private int? _requestedHashCode; private List? _domainEvents; public virtual Guid Id { get; protected set; } public IReadOnlyCollection DomainEvents => _domainEvents?.AsReadOnly() ?? Array.Empty().AsReadOnly(); public void AddDomainEvent(IDomainEvent eventItem) { _domainEvents ??= new List(); _domainEvents.Add(eventItem); } public void RemoveDomainEvent(IDomainEvent eventItem) { _domainEvents?.Remove(eventItem); } public void ClearDomainEvents() { _domainEvents?.Clear(); } public bool IsTransient() { return Id == default; } public override bool Equals(object? obj) { if (obj is not Entity other) return false; if (ReferenceEquals(this, other)) return true; if (GetType() != other.GetType()) return false; if (IsTransient() || other.IsTransient()) return false; return Id.Equals(other.Id); } public override int GetHashCode() { if (!IsTransient()) { _requestedHashCode ??= Id.GetHashCode() ^ 31; return _requestedHashCode.Value; } return base.GetHashCode(); } public static bool operator ==(Entity? left, Entity? right) { return left?.Equals(right) ?? right is null; } public static bool operator !=(Entity? left, Entity? right) { return !(left == right); } } ``` ### Aggregate Root ```csharp /// /// EN: Marker interface for aggregate roots. /// VI: Interface đánh dấu aggregate roots. /// public interface IAggregateRoot { } /// /// EN: Order aggregate root with business rules. /// VI: Order aggregate root với business rules. /// public class Order : Entity, IAggregateRoot { private readonly List _orderItems = new(); public string UserId { get; private set; } public Address ShippingAddress { get; private set; } public OrderStatus Status { get; private set; } public decimal TotalAmount { get; private set; } public DateTime CreatedAt { get; private set; } public DateTime? SubmittedAt { get; private set; } public IReadOnlyCollection OrderItems => _orderItems.AsReadOnly(); // EN: Required by EF Core private Order() { } public Order(string userId, Address shippingAddress) { Id = Guid.NewGuid(); UserId = userId ?? throw new ArgumentNullException(nameof(userId)); ShippingAddress = shippingAddress ?? throw new ArgumentNullException(nameof(shippingAddress)); Status = OrderStatus.Draft; CreatedAt = DateTime.UtcNow; TotalAmount = 0; // EN: Raise domain event // VI: Raise domain event AddDomainEvent(new OrderCreatedDomainEvent(Id, userId)); } public void AddItem(Guid productId, int quantity, decimal unitPrice) { // EN: Business rule: Can only add items to draft orders // VI: Quy tắc: Chỉ thêm items vào orders draft if (Status != OrderStatus.Draft) throw new DomainException("Cannot add items to non-draft order"); if (quantity <= 0) throw new ArgumentException("Quantity must be positive", nameof(quantity)); if (unitPrice < 0) throw new ArgumentException("Price cannot be negative", nameof(unitPrice)); var existingItem = _orderItems.FirstOrDefault(i => i.ProductId == productId); if (existingItem != null) { existingItem.IncreaseQuantity(quantity); } else { _orderItems.Add(new OrderItem(productId, quantity, unitPrice)); } RecalculateTotal(); } public void RemoveItem(Guid productId) { if (Status != OrderStatus.Draft) throw new DomainException("Cannot remove items from non-draft order"); var item = _orderItems.FirstOrDefault(i => i.ProductId == productId); if (item != null) { _orderItems.Remove(item); RecalculateTotal(); } } public void Submit() { // EN: Business rule: Cannot submit empty order // VI: Quy tắc: Không thể submit order trống if (!_orderItems.Any()) throw new DomainException("Cannot submit empty order"); if (Status != OrderStatus.Draft) throw new DomainException($"Cannot submit order in {Status} status"); Status = OrderStatus.Submitted; SubmittedAt = DateTime.UtcNow; AddDomainEvent(new OrderSubmittedDomainEvent(Id, UserId, TotalAmount)); } public void Cancel(string reason) { if (Status == OrderStatus.Shipped || Status == OrderStatus.Delivered) throw new DomainException("Cannot cancel shipped or delivered order"); Status = OrderStatus.Cancelled; AddDomainEvent(new OrderCancelledDomainEvent(Id, reason)); } private void RecalculateTotal() { TotalAmount = _orderItems.Sum(i => i.Quantity * i.UnitPrice); } } ``` ### Value Object ```csharp /// /// EN: Base class for value objects. /// VI: Base class cho value objects. /// public abstract class ValueObject { protected abstract IEnumerable GetEqualityComponents(); public override bool Equals(object? obj) { if (obj is null || obj.GetType() != GetType()) return false; var other = (ValueObject)obj; return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); } public override int GetHashCode() { return GetEqualityComponents() .Select(x => x?.GetHashCode() ?? 0) .Aggregate((x, y) => x ^ y); } public static bool operator ==(ValueObject? left, ValueObject? right) { return left?.Equals(right) ?? right is null; } public static bool operator !=(ValueObject? left, ValueObject? right) { return !(left == right); } } /// /// EN: Address value object. /// VI: Value object địa chỉ. /// public class Address : ValueObject { public string Street { get; } public string City { get; } public string State { get; } public string PostalCode { get; } public string Country { get; } public Address(string street, string city, string state, string postalCode, string country) { Street = street ?? throw new ArgumentNullException(nameof(street)); City = city ?? throw new ArgumentNullException(nameof(city)); State = state ?? throw new ArgumentNullException(nameof(state)); PostalCode = postalCode ?? throw new ArgumentNullException(nameof(postalCode)); Country = country ?? throw new ArgumentNullException(nameof(country)); } protected override IEnumerable GetEqualityComponents() { yield return Street; yield return City; yield return State; yield return PostalCode; yield return Country; } } /// /// EN: Money value object with currency. /// VI: Value object tiền tệ. /// 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"); Amount = amount; Currency = currency?.ToUpperInvariant() ?? throw new ArgumentNullException(nameof(currency)); } public Money Add(Money other) { if (Currency != other.Currency) throw new InvalidOperationException("Cannot add different currencies"); return new Money(Amount + other.Amount, Currency); } protected override IEnumerable GetEqualityComponents() { yield return Amount; yield return Currency; } } ``` ### Domain Events ```csharp /// /// EN: Domain event interface. /// VI: Interface domain event. /// public interface IDomainEvent : INotification { Guid Id { get; } DateTime OccurredOn { get; } } /// /// EN: Domain event when order is submitted. /// VI: Domain event khi order được submit. /// public record OrderSubmittedDomainEvent : IDomainEvent { public Guid Id { get; } = Guid.NewGuid(); public DateTime OccurredOn { get; } = DateTime.UtcNow; public Guid OrderId { get; init; } public string UserId { get; init; } public decimal TotalAmount { get; init; } public OrderSubmittedDomainEvent(Guid orderId, string userId, decimal totalAmount) { OrderId = orderId; UserId = userId; TotalAmount = totalAmount; } } ``` ## Common Mistakes / Lỗi Thường Gặp ### 1. Anemic Domain Model ```csharp // ❌ BAD: Anemic model with no behavior public class Order { public Guid Id { get; set; } public string Status { get; set; } public List Items { get; set; } } // ✅ GOOD: Rich domain model with behavior public class Order { private readonly List _items = new(); public OrderStatus Status { get; private set; } public void AddItem(Guid productId, int quantity, decimal price) { if (Status != OrderStatus.Draft) throw new DomainException("Cannot modify non-draft order"); // ... } } ``` ### 2. Direct Property Modification ```csharp // ❌ BAD: Direct modification bypasses rules order.Status = OrderStatus.Submitted; // ✅ GOOD: Use domain methods order.Submit(); ``` ### 3. Logic in Application Layer ```csharp // ❌ BAD: Business logic in handler public async Task Handle(CreateOrderCommand cmd) { if (cmd.Items.Count == 0) throw new Exception("Empty order"); // ... } // ✅ GOOD: Business logic in domain public void Submit() { if (!_orderItems.Any()) throw new DomainException("Cannot submit empty order"); } ``` ## Quick Reference / Tham Chiếu Nhanh ### Aggregate Design Guidelines | Guideline | Description | |-----------|-------------| | Small aggregates | Keep aggregates focused | | Reference by ID | 외부 aggregates chỉ tham chiếu qua ID | | Eventual consistency | Between aggregates | | Immediate consistency | Within aggregate | ### When to Use Each Pattern | Pattern | Use When | |---------|----------| | Entity | Has identity, lifecycle | | Value Object | No identity, immutable | | Aggregate | Group of related entities | | Domain Event | Side effects needed | | Domain Service | Logic doesn't fit entity | ## Resources / Tài Nguyên - [Detailed Examples](./references/REFERENCE.md) - Full code examples - [Repository Pattern](../repository-pattern/SKILL.md) - Data access - [CQRS MediatR](../cqrs-mediatr/SKILL.md) - Command handlers - [Testing Patterns](../testing-patterns/SKILL.md) - Domain testing