Domain-Driven Design (DDD) is a software development approach that focuses on modeling software to match a domain according to input from that domain's experts. It's particularly valuable for complex business applications where the domain logic is intricate and requires deep understanding.
DDD is a methodology that connects implementation to an evolving model. It emphasizes:
The ubiquitous language is a common vocabulary used by all team members to connect all activities of the team with the software.
// Good: Uses domain language
public class Order
{
public OrderId Id { get; private set; }
public CustomerId CustomerId { get; private set; }
public OrderStatus Status { get; private set; }
public Money TotalAmount { get; private set; }
public void MarkAsShipped()
{
if (Status != OrderStatus.Confirmed)
throw new InvalidOperationException("Only confirmed orders can be shipped");
Status = OrderStatus.Shipped;
}
}A bounded context is an explicit boundary within which a domain model is valid. Different contexts may have different models for the same concept.
// Order Context
namespace OrderManagement.Domain.Orders
{
public class Order
{
// Order-specific properties and behavior
}
}
// Shipping Context
namespace Shipping.Domain.Shipments
{
public class Shipment
{
// Shipping-specific properties and behavior
}
}Entities have identity and lifecycle:
public class Customer : Entity<CustomerId>
{
public string Name { get; private set; }
public Email Email { get; private set; }
public Customer(CustomerId id, string name, Email email)
{
Id = id;
Name = name;
Email = email;
}
}Value Objects are defined by their attributes:
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");
Amount = amount;
Currency = currency;
}
public Money Add(Money other)
{
if (Currency != other.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;
}
}Aggregates are clusters of entities and value objects treated as a single unit:
public class Order : AggregateRoot<OrderId>
{
private readonly List<OrderItem> _items = new();
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
public OrderStatus Status { get; private set; }
public Money TotalAmount { get; private set; }
public void AddItem(ProductId productId, int quantity, Money unitPrice)
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot modify confirmed order");
var item = new OrderItem(productId, quantity, unitPrice);
_items.Add(item);
RecalculateTotal();
}
public void Confirm()
{
if (_items.Count == 0)
throw new InvalidOperationException("Cannot confirm empty order");
Status = OrderStatus.Confirmed;
AddDomainEvent(new OrderConfirmedEvent(Id));
}
private void RecalculateTotal()
{
TotalAmount = _items.Aggregate(
Money.Zero(Currency.USD),
(total, item) => total.Add(item.Subtotal)
);
}
}Domain services contain domain logic that doesn't naturally fit within entities:
public interface IOrderPricingService
{
Money CalculateTotal(Order order, Customer customer);
}
public class OrderPricingService : IOrderPricingService
{
private readonly IDiscountService _discountService;
public Money CalculateTotal(Order order, Customer customer)
{
var subtotal = order.Items
.Sum(item => item.Subtotal.Amount);
var discount = _discountService.CalculateDiscount(customer, subtotal);
return new Money(subtotal - discount, "USD");
}
}DDD typically uses a layered architecture:
┌─────────────────────────────────┐
│ Presentation Layer (UI/API) │
├─────────────────────────────────┤
│ Application Layer (Use Cases)│
├─────────────────────────────────┤
│ Domain Layer (Business Logic) │
├─────────────────────────────────┤
│ Infrastructure Layer (Data) │
└─────────────────────────────────┘
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, OrderId>
{
private readonly IOrderRepository _orderRepository;
private readonly IUnitOfWork _unitOfWork;
public async Task<OrderId> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
var order = new Order(
OrderId.NewId(),
new CustomerId(request.CustomerId),
request.Items.Select(i =>
new OrderItem(
new ProductId(i.ProductId),
i.Quantity,
new Money(i.Price, "USD")
)
)
);
await _orderRepository.AddAsync(order);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return order.Id;
}
}OrderManagement.Domain/
├── Entities/
├── ValueObjects/
├── Aggregates/
├── DomainServices/
└── DomainEvents/
OrderManagement.Application/
├── Commands/
├── Queries/
└── DTOs/
OrderManagement.Infrastructure/
├── Persistence/
└── ExternalServices/
OrderManagement.API/
├── Controllers/
└── Program.cs
public abstract class DomainEvent
{
public DateTime OccurredOn { get; } = DateTime.UtcNow;
}
public class OrderConfirmedEvent : DomainEvent
{
public OrderId OrderId { get; }
public OrderConfirmedEvent(OrderId orderId)
{
OrderId = orderId;
}
}DDD is most valuable when:
Domain-Driven Design provides a powerful approach to building software that closely aligns with business needs. By focusing on the domain, using ubiquitous language, and properly structuring code, you can create maintainable, scalable applications that accurately represent business requirements.