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
- Ubiquitous Language: Reflect business terms in code
- Bounded Contexts: Separate domains with clear boundaries
- Value Objects: Model business concepts
- Aggregates: Enforce business rules
- Repositories: Abstract persistence
- 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.