Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

BenefitDescription
Business FocusCode mirrors business language
Complex DomainsBetter handling of complex business logic
CommunicationImproved team communication
TestabilityDomain logic is easily testable
FlexibilityEasier 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