An implementation of Domain-Driven Design Tactical Patterns.
- Evans, Eric. "Domain-Driven Design: Tackling Complexity in the Heart of Software"
- Vernon, Vaughn. "Domain-Driven Design Distilled"
- Vernon, Vaughn. "Implementing Domain-Driven Design"
Objects defined by their identity, not their attributes.
public abstract class Entity<TId> : IEquatable<Entity<TId>>
{
public TId Id { get; protected set; }
// Identity-based equality
}Immutable objects defined by their attributes.
public class Money : ValueObject
{
public decimal Amount { get; }
public string Currency { get; }
// Attribute-based equality
}Consistency boundaries with a single entry point.
public class Order : AggregateRoot<OrderId>
{
private readonly List<OrderItem> _orderItems = new();
// Only Order can modify OrderItems
public void AddItem(ProductId productId, ...) { ... }
}Facts about what happened in the domain.
public record OrderSubmittedDomainEvent : DomainEventBase
{
public OrderId OrderId { get; }
public decimal TotalAmount { get; }
}Collection-like interface for aggregates.
public interface IOrderRepository : IRepository<Order, OrderId>
{
Task<IReadOnlyList<Order>> GetByCustomerIdAsync(CustomerId customerId);
}Stateless operations that don't belong to entities.
public interface IPaymentProcessingService : IDomainService
{
PaymentValidationResult ValidatePayment(Payment payment);
Money CalculateProcessingFee(Money amount, PaymentMethod method);
}Encapsulated business rules for querying and filtering.
public class OrderReadyForProcessingSpecification : Specification<Order>
{
public override Expression<Func<Order, bool>> ToExpression()
=> order => order.Status == OrderStatus.Paid;
}
// Usage: Filtering/Querying
var overdueOrders = await repository.FindAsync(new OverdueOrderSpecification(24));
var cancellableOrders = await repository.FindAsync(new CancellableOrderSpecification());
// Composable with And, Or, Not
var spec = new MinimumOrderValueSpecification(100) & new CancellableOrderSpecification();Encapsulated object creation.
public static Order Create(CustomerId customerId, Address address)
{
var order = new Order { ... };
order.RaiseDomainEvent(new OrderCreatedDomainEvent(...));
return order;
}Type-safe, behavior-rich enumerations.
public class OrderStatus : Enumeration
{
public static OrderStatus Draft = new(1, nameof(Draft));
public static OrderStatus Submitted = new(2, nameof(Submitted));
public bool CanBeCancelled() => this == Draft || this == Submitted;
}- Order Submitted: Order service raises
OrderSubmittedDomainEvent - Domain Event Handler: Converts to
OrderSubmittedIntegrationEvent - Event Bus: Publishes integration event
- Payment Handler: Creates and processes payment
- Payment Completed: Payment service raises
PaymentCompletedDomainEvent - Integration Event:
PaymentCompletedIntegrationEventpublished - Order Handler: Updates order status to Paid
┌─────────────────┐ ┌─────────────────┐
│ Order Service │ │ Payment Service │
├─────────────────┤ ├─────────────────┤
│ Order.Submit() │ │ │
│ │ │ │ │
│ ▼ │ │ │
│ OrderSubmitted │ ──Integration──► │ Create Payment │
│ DomainEvent │ Event │ │ │
│ │ │ ▼ │
│ │ │ Process Payment │
│ │ │ │ │
│ │ │ ▼ │
│ MarkAsPaid() │ ◄──Integration── │ PaymentCompleted│
│ │ │ Event │ DomainEvent │
│ ▼ │ │ │
│ Status = Paid │ │ │
└─────────────────┘ └─────────────────┘
cd src/Services/Order/Order.API
dotnet runAccess Swagger UI at: https://localhost:5001/swagger
cd src/Services/Payment/Payment.API
dotnet runAccess Swagger UI at: https://localhost:5002/swagger
Start infrastructure services (MongoDB):
docker-compose --profile infra up -d