Files
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

15 KiB

name, description, compatibility, metadata
name description compatibility metadata
domain-driven-design DDD patterns cho complex business logic. Use for Aggregates, Value Objects, Entities, Domain Events, và Rich Domain Model. .NET 8+, EF Core 8+
author version
Velik Ho 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

/// <summary>
/// EN: Base class for all entities.
/// VI: Base class cho tất cả entities.
/// </summary>
public abstract class Entity
{
    private int? _requestedHashCode;
    private List<IDomainEvent>? _domainEvents;

    public virtual Guid Id { get; protected set; }
    
    public IReadOnlyCollection<IDomainEvent> DomainEvents 
        => _domainEvents?.AsReadOnly() ?? Array.Empty<IDomainEvent>().AsReadOnly();

    public void AddDomainEvent(IDomainEvent eventItem)
    {
        _domainEvents ??= new List<IDomainEvent>();
        _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

/// <summary>
/// EN: Marker interface for aggregate roots.
/// VI: Interface đánh dấu aggregate roots.
/// </summary>
public interface IAggregateRoot { }

/// <summary>
/// EN: Order aggregate root with business rules.
/// VI: Order aggregate root với business rules.
/// </summary>
public class Order : Entity, IAggregateRoot
{
    private readonly List<OrderItem> _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<OrderItem> 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

/// <summary>
/// EN: Base class for value objects.
/// VI: Base class cho value objects.
/// </summary>
public abstract class ValueObject
{
    protected abstract IEnumerable<object?> 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);
    }
}

/// <summary>
/// EN: Address value object.
/// VI: Value object địa chỉ.
/// </summary>
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<object?> GetEqualityComponents()
    {
        yield return Street;
        yield return City;
        yield return State;
        yield return PostalCode;
        yield return Country;
    }
}

/// <summary>
/// EN: Money value object with currency.
/// VI: Value object tiền tệ.
/// </summary>
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<object?> GetEqualityComponents()
    {
        yield return Amount;
        yield return Currency;
    }
}

Domain Events

/// <summary>
/// EN: Domain event interface.
/// VI: Interface domain event.
/// </summary>
public interface IDomainEvent : INotification
{
    Guid Id { get; }
    DateTime OccurredOn { get; }
}

/// <summary>
/// EN: Domain event when order is submitted.
/// VI: Domain event khi order được submit.
/// </summary>
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

// ❌ BAD: Anemic model with no behavior
public class Order
{
    public Guid Id { get; set; }
    public string Status { get; set; }
    public List<OrderItem> Items { get; set; }
}

// ✅ GOOD: Rich domain model with behavior
public class Order
{
    private readonly List<OrderItem> _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

// ❌ BAD: Direct modification bypasses rules
order.Status = OrderStatus.Submitted;

// ✅ GOOD: Use domain methods
order.Submit();

3. Logic in Application Layer

// ❌ 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