Isaac.

Domain-Driven Design with ASP.NET Core

Design complex systems using Domain-Driven Design principles.

By EMEPublished: February 20, 2025
domain-driven designdddaspnet corearchitecturemodeling

A Simple Analogy

DDD is like designing a city around how people actually live. Understand the business (the city), organize it into districts (domains), and build structures (code) that match how people work.


DDD Core Concepts

| Concept | Meaning | |---------|---------| | Entity | Has identity, mutable | | Value Object | No identity, immutable | | Aggregate | Group of entities/values | | Repository | Collection-like persistence | | Service | Stateless logic | | Ubiquitous Language | Shared business vocabulary |


Value Objects

// Immutable, has no identity, compared by value
public class Money : ValueObject
{
    public decimal Amount { get; private set; }
    public string Currency { get; private set; }

    public Money(decimal amount, string currency)
    {
        if (amount < 0) throw new ArgumentException("Amount cannot be negative");
        if (string.IsNullOrEmpty(currency)) throw new ArgumentNullException(nameof(currency));
        
        Amount = amount;
        Currency = currency;
    }

    public Money Add(Money other)
    {
        if (other.Currency != 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;
    }
}

// Usage
var price = new Money(99.99m, "USD");
var discount = new Money(10m, "USD");
var finalPrice = price.Add(discount); // new Money(109.99, "USD")

Entities

// Has identity, mutable
public class Order : Entity<OrderId>
{
    public CustomerId CustomerId { get; private set; }
    public List<OrderItem> Items { get; private set; } = new();
    public OrderStatus Status { get; private set; }
    public Money Total { get; private set; }

    public static Order Create(CustomerId customerId)
    {
        return new Order
        {
            Id = OrderId.New(),
            CustomerId = customerId,
            Status = OrderStatus.Pending,
            Total = new Money(0, "USD")
        };
    }

    public void AddItem(Product product, int quantity)
    {
        var itemPrice = new Money(product.Price * quantity, "USD");
        Items.Add(new OrderItem(product.Id, quantity, itemPrice));
        Total = Total.Add(itemPrice);
    }

    public void Confirm()
    {
        Status = OrderStatus.Confirmed;
        // Raise domain event
        AddDomainEvent(new OrderConfirmed(this));
    }
}

Aggregates

// Aggregate Root: Controls consistency
public class Customer : Entity<CustomerId>
{
    public string Name { get; private set; }
    private readonly List<Order> _orders = new();
    public IReadOnlyCollection<Order> Orders => _orders.AsReadOnly();

    public void PlaceOrder(Order order)
    {
        if (order.CustomerId != Id)
            throw new InvalidOperationException("Order doesn't belong to this customer");
        
        _orders.Add(order);
    }

    public decimal GetTotalSpent()
    {
        return _orders
            .Where(o => o.Status == OrderStatus.Confirmed)
            .Sum(o => o.Total.Amount);
    }
}

// Boundaries: Only modify Customer through aggregate root
// Don't directly modify internal _orders from outside

Repository Pattern

// Repository: Collection-like interface
public interface IOrderRepository
{
    void Add(Order order);
    void Update(Order order);
    void Remove(Order order);
    Task<Order> GetByIdAsync(OrderId id);
    Task<IEnumerable<Order>> GetByCustomerAsync(CustomerId customerId);
}

// Implementation
public class OrderRepository : IOrderRepository
{
    private readonly DbContext _context;

    public async Task<Order> GetByIdAsync(OrderId id)
    {
        return await _context.Orders
            .Include(o => o.Items)
            .FirstOrDefaultAsync(o => o.Id == id);
    }

    public void Add(Order order)
    {
        _context.Orders.Add(order);
    }
}

Domain Services

// Stateless business logic
public class PricingService
{
    public Money CalculateDiscount(Customer customer, Order order)
    {
        var totalSpent = customer.GetTotalSpent();
        
        return totalSpent switch
        {
            >= 10000 => new Money(order.Total.Amount * 0.2m, order.Total.Currency), // 20%
            >= 5000 => new Money(order.Total.Amount * 0.15m, order.Total.Currency), // 15%
            >= 1000 => new Money(order.Total.Amount * 0.1m, order.Total.Currency),  // 10%
            _ => new Money(0, order.Total.Currency)
        };
    }
}

Best Practices

  1. Ubiquitous Language: Reflect business terms in code
  2. Bounded Contexts: Separate domains with clear boundaries
  3. Value Objects: Model business concepts
  4. Aggregates: Enforce business rules
  5. Repositories: Abstract persistence
  6. Domain Events: Communicate changes

Related Concepts

  • Event sourcing for audit trails
  • CQRS for complex queries
  • Anti-corruption layers between domains
  • Event-driven architecture

Summary

Domain-Driven Design aligns code with business reality. Use entities, value objects, and aggregates to model complex domains and enforce business rules consistently.