Domain-Driven Design with ASP.NET Core
Design complex systems using Domain-Driven Design principles.
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.
Related Articles
Microservices with ASP.NET Core
Design and implement scalable microservices architecture.
Read More architectureMulti-Tenancy in ASP.NET Core
Build scalable multi-tenant applications serving multiple customers.
Read More architectureCQRS and Event Sourcing
Separate reads and writes with CQRS and Event Sourcing patterns.
Read More