What is Domain-Driven Design (DDD) and what are its core principles?

5 minadvancedDDDarchitecturedomain-modeling

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:

  1. Focus on the Domain

    • The domain is the heart of the software
    • Business logic should be the primary concern
    • Technical concerns are secondary
  2. 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
  3. 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:

  1. 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; }
}
  1. Context Mapping
    • Define relationships between bounded contexts
    • Shared Kernel, Customer-Supplier, Conformist, Anti-Corruption Layer

Tactical Design Patterns:

  1. 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));
    }
}
  1. 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);
    }
}
  1. 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));
    }
}
  1. 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;
    }
}
  1. 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:

  1. Better Communication: Ubiquitous language improves team communication
  2. Focused Design: Clear boundaries prevent complexity
  3. Business Alignment: Software reflects business understanding
  4. Maintainability: Well-structured domain models are easier to maintain
  5. 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

Related Resources