Domain Driven Design (DDD) for LLD - IndianTechnoEra
Latest update Android YouTube

Domain Driven Design (DDD) for LLD

Chapter 7: Domain Driven Design (DDD) Basics - Modeling Complex Business Domains

Series: Low Level Design for .NET Developers | Previous: Chapter 6: .NET Specific Architecture | Next: Chapter 8: Case Study - Parking Lot System


📖 Introduction

Domain Driven Design (DDD) is an approach to software development that focuses on modeling complex business domains. Instead of starting with technical concerns like databases or frameworks, DDD starts with the business domain itself. The goal is to create software that truly reflects the business processes, rules, and language.

In this chapter, we'll explore the tactical patterns of DDD that help you implement complex business logic in a maintainable way:

  • Ubiquitous Language - The foundation of DDD
  • Entities vs Value Objects - Understanding identity
  • Aggregates and Aggregate Roots - Maintaining consistency boundaries
  • Repositories - Persistence abstraction for aggregates
  • Domain Services - Business logic that doesn't belong in entities
  • Domain Events - Decoupling side effects
  • Specification Pattern - Business rules as first-class citizens
  • Factories - Complex object creation logic

1. Ubiquitous Language - The Foundation of DDD

Definition: Ubiquitous Language is a common language shared by developers and domain experts. It's used in code, documentation, and conversations. Every technical term should map directly to a business concept.

Example: In a banking system, instead of using technical terms like:

  • "Customer table" → Use "Account Holder"
  • "Transaction record" → Use "Ledger Entry"
  • "Process transfer" → Use "Money Transfer"

This alignment ensures that when a business expert says "a customer can transfer money between accounts," the developer knows exactly which classes and methods to work with.


2. Entities vs Value Objects

2.1 Entities - Objects with Identity

An Entity has a distinct identity that runs through time and different states. Two entities with the same attribute values are still different because they have different identities.

// Entity - Has identity (Id)
public class Customer : IAggregateRoot
{
    public int Id { get; private set; }  // Identity
    public string Name { get; private set; }
    public Email Email { get; private set; }  // Value Object
    public Address ShippingAddress { get; private set; }  // Value Object
    public CustomerStatus Status { get; private set; }
    public DateTime CreatedAt { get; private set; }
    public DateTime? UpdatedAt { get; private set; }
    
    private readonly List<Order> _orders = new List<Order>();
    public IReadOnlyCollection<Order> Orders => _orders.AsReadOnly();
    
    // Private constructor for EF Core
    private Customer() { }
    
    public Customer(string name, Email email, Address shippingAddress)
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new ArgumentException("Customer name is required", nameof(name));
            
        Name = name;
        Email = email ?? throw new ArgumentNullException(nameof(email));
        ShippingAddress = shippingAddress ?? throw new ArgumentNullException(nameof(shippingAddress));
        Status = CustomerStatus.Active;
        CreatedAt = DateTime.UtcNow;
    }
    
    public void UpdateContactInfo(string name, Email email)
    {
        Name = name;
        Email = email;
        UpdatedAt = DateTime.UtcNow;
    }
    
    public void UpdateShippingAddress(Address newAddress)
    {
        ShippingAddress = newAddress ?? throw new ArgumentNullException(nameof(newAddress));
        UpdatedAt = DateTime.UtcNow;
    }
    
    public void Deactivate()
    {
        if (Status == CustomerStatus.Inactive)
            throw new BusinessException("Customer is already inactive");
            
        if (_orders.Any(o => o.Status == OrderStatus.Pending || o.Status == OrderStatus.Processing))
            throw new BusinessException("Cannot deactivate customer with pending orders");
            
        Status = CustomerStatus.Inactive;
        UpdatedAt = DateTime.UtcNow;
    }
    
    public void AddOrder(Order order)
    {
        if (order == null) throw new ArgumentNullException(nameof(order));
        if (Status != CustomerStatus.Active)
            throw new BusinessException("Inactive customers cannot place orders");
            
        _orders.Add(order);
        UpdatedAt = DateTime.UtcNow;
    }
    
    public override bool Equals(object obj)
    {
        if (obj is not Customer other) return false;
        if (ReferenceEquals(this, other)) return true;
        if (GetType() != other.GetType()) return false;
        
        return Id == other.Id;
    }
    
    public override int GetHashCode() => Id.GetHashCode();
}

public enum CustomerStatus
{
    Active,
    Inactive,
    Suspended
}

2.2 Value Objects - Immutable by Nature

A Value Object has no identity. It is defined entirely by its attributes. Two value objects with the same values are considered equal.

// Value Object - No identity, immutable
public class Email : IEquatable<Email>
{
    public string Value { get; }
    
    public Email(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
            throw new ArgumentException("Email cannot be empty", nameof(value));
            
        if (!IsValidEmail(value))
            throw new ArgumentException("Invalid email format", nameof(value));
            
        Value = value.ToLowerInvariant();
    }
    
    private static bool IsValidEmail(string email)
    {
        try
        {
            var addr = new System.Net.Mail.MailAddress(email);
            return addr.Address == email;
        }
        catch
        {
            return false;
        }
    }
    
    public override bool Equals(object obj)
    {
        return Equals(obj as Email);
    }
    
    public bool Equals(Email other)
    {
        return other != null && Value == other.Value;
    }
    
    public override int GetHashCode()
    {
        return Value.GetHashCode();
    }
    
    public override string ToString() => Value;
    
    public static implicit operator string(Email email) => email.Value;
    public static explicit operator Email(string value) => new Email(value);
}

// Value Object with multiple properties - using C# records
public record Address
{
    public string Street { get; }
    public string City { get; }
    public string State { get; }
    public string ZipCode { get; }
    public string Country { get; }
    
    public Address(string street, string city, string state, string zipCode, string country)
    {
        Street = street ?? throw new ArgumentNullException(nameof(street));
        City = city ?? throw new ArgumentNullException(nameof(city));
        State = state ?? throw new ArgumentNullException(nameof(state));
        ZipCode = zipCode ?? throw new ArgumentNullException(nameof(zipCode));
        Country = country ?? throw new ArgumentNullException(nameof(country));
        
        Validate();
    }
    
    private void Validate()
    {
        if (string.IsNullOrWhiteSpace(Street))
            throw new ArgumentException("Street is required", nameof(Street));
            
        if (string.IsNullOrWhiteSpace(City))
            throw new ArgumentException("City is required", nameof(City));
            
        if (string.IsNullOrWhiteSpace(ZipCode))
            throw new ArgumentException("ZipCode is required", nameof(ZipCode));
    }
    
    public Address WithZipCode(string newZipCode)
    {
        return new Address(Street, City, State, newZipCode, Country);
    }
    
    public override string ToString()
    {
        return $"{Street}, {City}, {State} {ZipCode}, {Country}";
    }
}

// Value Object with business logic
public record Money
{
    public decimal Amount { get; }
    public string Currency { get; }
    
    public Money(decimal amount, string currency)
    {
        if (amount < 0)
            throw new ArgumentException("Amount cannot be negative", nameof(amount));
            
        if (string.IsNullOrWhiteSpace(currency))
            throw new ArgumentException("Currency is required", nameof(currency));
            
        if (currency.Length != 3)
            throw new ArgumentException("Currency must be ISO 4217 code", nameof(currency));
            
        Amount = amount;
        Currency = currency.ToUpperInvariant();
    }
    
    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new BusinessException($"Cannot add different currencies: {Currency} and {other.Currency}");
            
        return new Money(Amount + other.Amount, Currency);
    }
    
    public Money Subtract(Money other)
    {
        if (Currency != other.Currency)
            throw new BusinessException($"Cannot subtract different currencies: {Currency} and {other.Currency}");
            
        return new Money(Amount - other.Amount, Currency);
    }
    
    public Money Multiply(decimal multiplier)
    {
        return new Money(Amount * multiplier, Currency);
    }
    
    public static Money Zero(string currency) => new Money(0, currency);
    
    public override string ToString() => $"{Amount:F2} {Currency}";
}

2.3 Entity vs Value Object Comparison

Criteria Entity Value Object
Identity Has unique identity (Id) No identity, defined by attributes
Mutability Mutable over time Immutable
Equality Based on identity Based on all attributes
Lifecycle Can change over time Created and discarded
Examples Customer, Order, Product Email, Address, Money

3. Aggregates and Aggregate Roots

An Aggregate is a cluster of domain objects that can be treated as a single unit. The Aggregate Root is the only entry point to the aggregate, ensuring consistency and invariants.

// Aggregate Root - Order
public class Order : IAggregateRoot
{
    private readonly List<OrderItem> _items = new List<OrderItem>();
    private readonly List<DomainEvent> _domainEvents = new List<DomainEvent>();
    
    public int Id { get; private set; }
    public int CustomerId { get; private set; }
    public string OrderNumber { get; private set; }
    public DateTime CreatedAt { get; private set; }
    public DateTime? PaidAt { get; private set; }
    public DateTime? ShippedAt { get; private set; }
    public DateTime? DeliveredAt { get; private set; }
    public OrderStatus Status { get; private set; }
    public Money TotalAmount { get; private set; }
    public ShippingAddress ShippingAddress { get; private set; }
    public PaymentMethod PaymentMethod { get; private set; }
    public string? TrackingNumber { get; private set; }
    
    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
    public IReadOnlyCollection<DomainEvent> DomainEvents => _domainEvents.AsReadOnly();
    
    private Order() { } // EF Core
    
    public Order(int customerId, ShippingAddress shippingAddress, PaymentMethod paymentMethod)
    {
        CustomerId = customerId;
        OrderNumber = GenerateOrderNumber();
        CreatedAt = DateTime.UtcNow;
        Status = OrderStatus.Pending;
        ShippingAddress = shippingAddress ?? throw new ArgumentNullException(nameof(shippingAddress));
        PaymentMethod = paymentMethod;
        TotalAmount = Money.Zero("USD");
        
        AddDomainEvent(new OrderCreatedEvent(this));
    }
    
    public void AddItem(Product product, int quantity)
    {
        if (product == null) throw new ArgumentNullException(nameof(product));
        if (quantity <= 0) throw new ArgumentException("Quantity must be positive", nameof(quantity));
        
        if (Status != OrderStatus.Pending)
            throw new BusinessException("Cannot add items to an order that is already processing");
            
        if (product.StockQuantity < quantity)
            throw new BusinessException($"Insufficient stock for product {product.Name}");
            
        var existingItem = _items.FirstOrDefault(i => i.ProductId == product.Id);
        
        if (existingItem != null)
        {
            existingItem.IncreaseQuantity(quantity);
        }
        else
        {
            _items.Add(new OrderItem(product.Id, product.Name, product.Price, quantity));
        }
        
        RecalculateTotal();
        
        AddDomainEvent(new OrderItemAddedEvent(this, product.Id, quantity));
    }
    
    public void RemoveItem(int productId)
    {
        var item = _items.FirstOrDefault(i => i.ProductId == productId);
        if (item == null)
            throw new BusinessException($"Product {productId} not found in order");
            
        _items.Remove(item);
        RecalculateTotal();
        
        AddDomainEvent(new OrderItemRemovedEvent(this, productId));
    }
    
    public void Confirm()
    {
        if (Status != OrderStatus.Pending)
            throw new BusinessException($"Cannot confirm order in {Status} status");
            
        if (!_items.Any())
            throw new BusinessException("Cannot confirm empty order");
            
        Status = OrderStatus.Confirmed;
        
        AddDomainEvent(new OrderConfirmedEvent(this));
    }
    
    public void ProcessPayment(string transactionId)
    {
        if (Status != OrderStatus.Confirmed)
            throw new BusinessException($"Cannot process payment for order in {Status} status");
            
        PaidAt = DateTime.UtcNow;
        Status = OrderStatus.Paid;
        
        AddDomainEvent(new OrderPaidEvent(this, transactionId));
    }
    
    public void Ship(string trackingNumber)
    {
        if (Status != OrderStatus.Paid)
            throw new BusinessException($"Cannot ship order in {Status} status");
            
        if (string.IsNullOrWhiteSpace(trackingNumber))
            throw new ArgumentException("Tracking number is required", nameof(trackingNumber));
            
        TrackingNumber = trackingNumber;
        ShippedAt = DateTime.UtcNow;
        Status = OrderStatus.Shipped;
        
        AddDomainEvent(new OrderShippedEvent(this, trackingNumber));
    }
    
    public void Deliver()
    {
        if (Status != OrderStatus.Shipped)
            throw new BusinessException($"Cannot deliver order in {Status} status");
            
        DeliveredAt = DateTime.UtcNow;
        Status = OrderStatus.Delivered;
        
        AddDomainEvent(new OrderDeliveredEvent(this));
    }
    
    public void Cancel(string reason)
    {
        if (Status == OrderStatus.Delivered)
            throw new BusinessException("Cannot cancel delivered order");
            
        if (Status == OrderStatus.Shipped)
            throw new BusinessException("Cannot cancel shipped order, please initiate return");
            
        Status = OrderStatus.Cancelled;
        
        AddDomainEvent(new OrderCancelledEvent(this, reason));
    }
    
    private void RecalculateTotal()
    {
        var total = _items.Sum(i => i.TotalPrice.Amount);
        TotalAmount = new Money(total, "USD");
    }
    
    private static string GenerateOrderNumber()
    {
        return $"ORD-{DateTime.Now:yyyyMMdd}-{Guid.NewGuid().ToString().Substring(0, 8).ToUpper()}";
    }
    
    private void AddDomainEvent(DomainEvent eventItem)
    {
        _domainEvents.Add(eventItem);
    }
    
    public void ClearDomainEvents()
    {
        _domainEvents.Clear();
    }
}

// Entity within Aggregate - OrderItem (not aggregate root)
public class OrderItem
{
    public int Id { get; private set; }
    public int ProductId { get; private set; }
    public string ProductName { get; private set; }
    public Money UnitPrice { get; private set; }
    public int Quantity { get; private set; }
    public Money TotalPrice => UnitPrice.Multiply(Quantity);
    
    private OrderItem() { } // EF Core
    
    public OrderItem(int productId, string productName, Money unitPrice, int quantity)
    {
        ProductId = productId;
        ProductName = productName ?? throw new ArgumentNullException(nameof(productName));
        UnitPrice = unitPrice ?? throw new ArgumentNullException(nameof(unitPrice));
        Quantity = quantity;
    }
    
    public void IncreaseQuantity(int amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Amount must be positive", nameof(amount));
            
        Quantity += amount;
    }
    
    public void DecreaseQuantity(int amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Amount must be positive", nameof(amount));
            
        if (amount > Quantity)
            throw new BusinessException($"Cannot decrease quantity below zero");
            
        Quantity -= amount;
    }
}

// Supporting value objects
public class ShippingAddress : ValueObject
{
    public string Street { get; }
    public string City { get; }
    public string State { get; }
    public string ZipCode { get; }
    public string Country { get; }
    
    public ShippingAddress(string street, string city, string state, string zipCode, string country)
    {
        Street = street;
        City = city;
        State = state;
        ZipCode = zipCode;
        Country = country;
    }
    
    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Street;
        yield return City;
        yield return State;
        yield return ZipCode;
        yield return Country;
    }
}

public enum OrderStatus
{
    Pending,
    Confirmed,
    Paid,
    Processing,
    Shipped,
    Delivered,
    Cancelled,
    Returned
}

public enum PaymentMethod
{
    CreditCard,
    PayPal,
    BankTransfer,
    CashOnDelivery
}

// Base Value Object class for equality
public abstract class ValueObject
{
    protected abstract IEnumerable<object> GetEqualityComponents();
    
    public override bool Equals(object obj)
    {
        if (obj == null || obj.GetType() != GetType()) return false;
        
        var other = (ValueObject)obj;
        return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
    }
    
    public override int GetHashCode()
    {
        return GetEqualityComponents()
            .Select(x => x?.GetHashCode() ?? 0)
            .Aggregate((x, y) => x ^ y);
    }
}

4. Repository Pattern for Aggregates

Repositories provide abstraction for persistence of aggregates. Each aggregate root has its own repository.

// Repository interface for aggregate root
public interface IOrderRepository
{
    Task<Order> GetByIdAsync(int id, CancellationToken cancellationToken = default);
    Task<Order> GetByOrderNumberAsync(string orderNumber, CancellationToken cancellationToken = default);
    Task<IEnumerable<Order>> GetByCustomerIdAsync(int customerId, CancellationToken cancellationToken = default);
    Task<IEnumerable<Order>> GetByStatusAsync(OrderStatus status, CancellationToken cancellationToken = default);
    Task<IEnumerable<Order>> GetPendingOrdersAsync(DateTime olderThan, CancellationToken cancellationToken = default);
    Task<bool> ExistsAsync(int id, CancellationToken cancellationToken = default);
    Task AddAsync(Order order, CancellationToken cancellationToken = default);
    Task UpdateAsync(Order order, CancellationToken cancellationToken = default);
    Task DeleteAsync(int id, CancellationToken cancellationToken = default);
    Task<int> GetCountByStatusAsync(OrderStatus status, CancellationToken cancellationToken = default);
}

// EF Core Implementation
public class OrderRepository : IOrderRepository
{
    private readonly ApplicationDbContext _context;
    private readonly DbSet<Order> _dbSet;
    
    public OrderRepository(ApplicationDbContext context)
    {
        _context = context;
        _dbSet = _context.Set<Order>();
    }
    
    public async Task<Order> GetByIdAsync(int id, CancellationToken cancellationToken = default)
    {
        return await _dbSet
            .Include(o => o.Items)
            .FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
    }
    
    public async Task<Order> GetByOrderNumberAsync(string orderNumber, CancellationToken cancellationToken = default)
    {
        return await _dbSet
            .Include(o => o.Items)
            .FirstOrDefaultAsync(o => o.OrderNumber == orderNumber, cancellationToken);
    }
    
    public async Task<IEnumerable<Order>> GetByCustomerIdAsync(int customerId, CancellationToken cancellationToken = default)
    {
        return await _dbSet
            .Include(o => o.Items)
            .Where(o => o.CustomerId == customerId)
            .OrderByDescending(o => o.CreatedAt)
            .ToListAsync(cancellationToken);
    }
    
    public async Task<IEnumerable<Order>> GetByStatusAsync(OrderStatus status, CancellationToken cancellationToken = default)
    {
        return await _dbSet
            .Include(o => o.Items)
            .Where(o => o.Status == status)
            .OrderByDescending(o => o.CreatedAt)
            .ToListAsync(cancellationToken);
    }
    
    public async Task<IEnumerable<Order>> GetPendingOrdersAsync(DateTime olderThan, CancellationToken cancellationToken = default)
    {
        return await _dbSet
            .Include(o => o.Items)
            .Where(o => o.Status == OrderStatus.Pending && o.CreatedAt < olderThan)
            .OrderBy(o => o.CreatedAt)
            .ToListAsync(cancellationToken);
    }
    
    public async Task<bool> ExistsAsync(int id, CancellationToken cancellationToken = default)
    {
        return await _dbSet.AnyAsync(o => o.Id == id, cancellationToken);
    }
    
    public async Task AddAsync(Order order, CancellationToken cancellationToken = default)
    {
        await _dbSet.AddAsync(order, cancellationToken);
    }
    
    public Task UpdateAsync(Order order, CancellationToken cancellationToken = default)
    {
        _dbSet.Update(order);
        return Task.CompletedTask;
    }
    
    public async Task DeleteAsync(int id, CancellationToken cancellationToken = default)
    {
        var order = await GetByIdAsync(id, cancellationToken);
        if (order != null)
        {
            _dbSet.Remove(order);
        }
    }
    
    public async Task<int> GetCountByStatusAsync(OrderStatus status, CancellationToken cancellationToken = default)
    {
        return await _dbSet.CountAsync(o => o.Status == status, cancellationToken);
    }
}

// Specification pattern for advanced queries
public interface ISpecification<T>
{
    Expression<Func<T, bool>> Criteria { get; }
    List<Expression<Func<T, object>>> Includes { get; }
    List<string> IncludeStrings { get; }
    Expression<Func<T, object>> OrderBy { get; }
    Expression<Func<T, object>> OrderByDescending { get; }
    int Take { get; }
    int Skip { get; }
    bool IsPagingEnabled { get; }
}

public class OrdersByCustomerSpecification : BaseSpecification<Order>
{
    public OrdersByCustomerSpecification(int customerId, bool includeCompleted = false)
    {
        AddCriteria(o => o.CustomerId == customerId);
        
        if (!includeCompleted)
        {
            AddCriteria(o => o.Status != OrderStatus.Delivered && 
                            o.Status != OrderStatus.Cancelled);
        }
        
        AddInclude(o => o.Items);
        ApplyOrderByDescending(o => o.CreatedAt);
    }
}

// Unit of Work pattern for transaction management
public interface IUnitOfWork
{
    IOrderRepository Orders { get; }
    ICustomerRepository Customers { get; }
    IProductRepository Products { get; }
    Task<int> CompleteAsync(CancellationToken cancellationToken = default);
    Task BeginTransactionAsync(CancellationToken cancellationToken = default);
    Task CommitTransactionAsync(CancellationToken cancellationToken = default);
    Task RollbackTransactionAsync(CancellationToken cancellationToken = default);
}

public class UnitOfWork : IUnitOfWork
{
    private readonly ApplicationDbContext _context;
    private IOrderRepository _orders;
    private ICustomerRepository _customers;
    private IProductRepository _products;
    private IDbContextTransaction _transaction;
    
    public UnitOfWork(ApplicationDbContext context)
    {
        _context = context;
    }
    
    public IOrderRepository Orders => _orders ??= new OrderRepository(_context);
    public ICustomerRepository Customers => _customers ??= new CustomerRepository(_context);
    public IProductRepository Products => _products ??= new ProductRepository(_context);
    
    public async Task<int> CompleteAsync(CancellationToken cancellationToken = default)
    {
        return await _context.SaveChangesAsync(cancellationToken);
    }
    
    public async Task BeginTransactionAsync(CancellationToken cancellationToken = default)
    {
        _transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
    }
    
    public async Task CommitTransactionAsync(CancellationToken cancellationToken = default)
    {
        await _transaction?.CommitAsync(cancellationToken);
    }
    
    public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default)
    {
        await _transaction?.RollbackAsync(cancellationToken);
    }
    
    public void Dispose()
    {
        _transaction?.Dispose();
        _context.Dispose();
    }
}

5. Domain Services

Domain Services encapsulate business logic that doesn't naturally fit within an entity or value object. They often coordinate between multiple aggregates or handle domain operations.

// Domain Service for money transfer between accounts
public interface ITransferService
{
    Task<TransferResult> TransferMoneyAsync(
        int fromAccountId, 
        int toAccountId, 
        Money amount, 
        string reference,
        CancellationToken cancellationToken = default);
}

public class TransferService : ITransferService
{
    private readonly IAccountRepository _accountRepository;
    private readonly ITransactionRepository _transactionRepository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly ILogger<TransferService> _logger;
    
    public TransferService(
        IAccountRepository accountRepository,
        ITransactionRepository transactionRepository,
        IUnitOfWork unitOfWork,
        ILogger<TransferService> logger)
    {
        _accountRepository = accountRepository;
        _transactionRepository = transactionRepository;
        _unitOfWork = unitOfWork;
        _logger = logger;
    }
    
    public async Task<TransferResult> TransferMoneyAsync(
        int fromAccountId, 
        int toAccountId, 
        Money amount, 
        string reference,
        CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Starting transfer from account {FromAccount} to {ToAccount} for {Amount}",
            fromAccountId, toAccountId, amount);
            
        await _unitOfWork.BeginTransactionAsync(cancellationToken);
        
        try
        {
            // Get aggregates
            var fromAccount = await _accountRepository.GetByIdWithTransactionsAsync(fromAccountId, cancellationToken);
            var toAccount = await _accountRepository.GetByIdWithTransactionsAsync(toAccountId, cancellationToken);
            
            if (fromAccount == null)
                return TransferResult.Failure($"Source account {fromAccountId} not found");
                
            if (toAccount == null)
                return TransferResult.Failure($"Destination account {toAccountId} not found");
            
            // Perform domain operations
            var withdrawal = fromAccount.Withdraw(amount, reference);
            var deposit = toAccount.Deposit(amount, reference);
            
            // Create transaction record
            var transaction = new Transaction(
                fromAccountId, 
                toAccountId, 
                amount, 
                reference, 
                withdrawal.ReferenceNumber);
            
            await _transactionRepository.AddAsync(transaction, cancellationToken);
            
            // Update aggregates
            await _accountRepository.UpdateAsync(fromAccount, cancellationToken);
            await _accountRepository.UpdateAsync(toAccount, cancellationToken);
            
            await _unitOfWork.CompleteAsync(cancellationToken);
            await _unitOfWork.CommitTransactionAsync(cancellationToken);
            
            _logger.LogInformation("Transfer completed successfully. Reference: {Reference}", withdrawal.ReferenceNumber);
            
            return TransferResult.Success(withdrawal.ReferenceNumber);
        }
        catch (BusinessException ex)
        {
            _logger.LogWarning(ex, "Transfer failed due to business rule violation");
            await _unitOfWork.RollbackTransactionAsync(cancellationToken);
            return TransferResult.Failure(ex.Message);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Transfer failed due to technical error");
            await _unitOfWork.RollbackTransactionAsync(cancellationToken);
            return TransferResult.Failure("An unexpected error occurred");
        }
    }
}

// Another domain service example - Discount calculation
public interface IDiscountCalculator
{
    Money CalculateDiscount(Order order, Customer customer);
}

public class DiscountCalculator : IDiscountCalculator
{
    private readonly IEnumerable<IDiscountRule> _discountRules;
    
    public DiscountCalculator(IEnumerable<IDiscountRule> discountRules)
    {
        _discountRules = discountRules;
    }
    
    public Money CalculateDiscount(Order order, Customer customer)
    {
        var discount = Money.Zero("USD");
        
        foreach (var rule in _discountRules.OrderBy(r => r.Priority))
        {
            discount = discount.Add(rule.Apply(order, customer));
        }
        
        return discount;
    }
}

public interface IDiscountRule
{
    int Priority { get; }
    Money Apply(Order order, Customer customer);
}

public class NewCustomerDiscountRule : IDiscountRule
{
    public int Priority => 1;
    
    public Money Apply(Order order, Customer customer)
    {
        if (customer.IsNewCustomer && order.TotalAmount.Amount > 100)
        {
            return new Money(order.TotalAmount.Amount * 0.1m, "USD");
        }
        
        return Money.Zero("USD");
    }
}

public class BulkOrderDiscountRule : IDiscountRule
{
    public int Priority => 2;
    
    public Money Apply(Order order, Customer customer)
    {
        var totalItems = order.Items.Sum(i => i.Quantity);
        
        if (totalItems >= 10)
        {
            return new Money(order.TotalAmount.Amount * 0.15m, "USD");
        }
        
        if (totalItems >= 5)
        {
            return new Money(order.TotalAmount.Amount * 0.1m, "USD");
        }
        
        return Money.Zero("USD");
    }
}

public class LoyalCustomerDiscountRule : IDiscountRule
{
    public int Priority => 3;
    
    public Money Apply(Order order, Customer customer)
    {
        if (customer.TotalSpent.Amount > 10000)
        {
            return new Money(order.TotalAmount.Amount * 0.2m, "USD");
        }
        
        if (customer.TotalSpent.Amount > 5000)
        {
            return new Money(order.TotalAmount.Amount * 0.1m, "USD");
        }
        
        return Money.Zero("USD");
    }
}

6. Domain Events

Domain Events capture important business occurrences. They enable loose coupling between different parts of the domain.

// Base domain event
public abstract class DomainEvent
{
    public DateTime OccurredOn { get; } = DateTime.UtcNow;
    public Guid EventId { get; } = Guid.NewGuid();
}

// Concrete domain events
public class OrderCreatedEvent : DomainEvent
{
    public Order Order { get; }
    
    public OrderCreatedEvent(Order order)
    {
        Order = order;
    }
}

public class OrderPaidEvent : DomainEvent
{
    public Order Order { get; }
    public string TransactionId { get; }
    
    public OrderPaidEvent(Order order, string transactionId)
    {
        Order = order;
        TransactionId = transactionId;
    }
}

public class OrderShippedEvent : DomainEvent
{
    public Order Order { get; }
    public string TrackingNumber { get; }
    
    public OrderShippedEvent(Order order, string trackingNumber)
    {
        Order = order;
        TrackingNumber = trackingNumber;
    }
}

public class OrderDeliveredEvent : DomainEvent
{
    public Order Order { get; }
    
    public OrderDeliveredEvent(Order order)
    {
        Order = order;
    }
}

public class OrderCancelledEvent : DomainEvent
{
    public Order Order { get; }
    public string Reason { get; }
    
    public OrderCancelledEvent(Order order, string reason)
    {
        Order = order;
        Reason = reason;
    }
}

// Domain event dispatcher
public interface IDomainEventDispatcher
{
    Task DispatchAsync(DomainEvent domainEvent, CancellationToken cancellationToken = default);
}

public class DomainEventDispatcher : IDomainEventDispatcher
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<DomainEventDispatcher> _logger;
    
    public DomainEventDispatcher(IServiceProvider serviceProvider, ILogger<DomainEventDispatcher> logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
    }
    
    public async Task DispatchAsync(DomainEvent domainEvent, CancellationToken cancellationToken = default)
    {
        var handlerType = typeof(IDomainEventHandler<>).MakeGenericType(domainEvent.GetType());
        var handlers = _serviceProvider.GetServices(handlerType);
        
        foreach (var handler in handlers)
        {
            try
            {
                await (Task)handlerType.GetMethod("HandleAsync")
                    .Invoke(handler, new object[] { domainEvent, cancellationToken });
                    
                _logger.LogInformation("Event {EventType} handled by {HandlerType}", 
                    domainEvent.GetType().Name, handler.GetType().Name);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error handling event {EventType}", domainEvent.GetType().Name);
                // Decide whether to throw or continue based on requirements
            }
        }
    }
}

// Event handlers
public interface IDomainEventHandler<in TEvent> where TEvent : DomainEvent
{
    Task HandleAsync(TEvent domainEvent, CancellationToken cancellationToken = default);
}

public class OrderCreatedEventHandler : IDomainEventHandler<OrderCreatedEvent>
{
    private readonly IEmailService _emailService;
    private readonly IInventoryService _inventoryService;
    private readonly ILogger<OrderCreatedEventHandler> _logger;
    
    public OrderCreatedEventHandler(
        IEmailService emailService,
        IInventoryService inventoryService,
        ILogger<OrderCreatedEventHandler> logger)
    {
        _emailService = emailService;
        _inventoryService = inventoryService;
        _logger = logger;
    }
    
    public async Task HandleAsync(OrderCreatedEvent domainEvent, CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Handling OrderCreatedEvent for Order {OrderId}", domainEvent.Order.Id);
        
        // Reserve inventory
        await _inventoryService.ReserveStockAsync(domainEvent.Order, cancellationToken);
        
        // Send confirmation email
        await _emailService.SendOrderConfirmationAsync(
            domainEvent.Order.CustomerId,
            domainEvent.Order.Id,
            cancellationToken);
    }
}

public class OrderPaidEventHandler : IDomainEventHandler<OrderPaidEvent>
{
    private readonly IEmailService _emailService;
    private readonly IInventoryService _inventoryService;
    private readonly ILogger<OrderPaidEventHandler> _logger;
    
    public OrderPaidEventHandler(
        IEmailService emailService,
        IInventoryService inventoryService,
        ILogger<OrderPaidEventHandler> logger)
    {
        _emailService = emailService;
        _inventoryService = inventoryService;
        _logger = logger;
    }
    
    public async Task HandleAsync(OrderPaidEvent domainEvent, CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Handling OrderPaidEvent for Order {OrderId}", domainEvent.Order.Id);
        
        // Confirm inventory reservation
        await _inventoryService.ConfirmReservationAsync(domainEvent.Order, cancellationToken);
        
        // Send payment confirmation email
        await _emailService.SendPaymentConfirmationAsync(
            domainEvent.Order.CustomerId,
            domainEvent.Order.Id,
            domainEvent.TransactionId,
            cancellationToken);
        
        // Notify warehouse
        await _inventoryService.NotifyWarehouseForPackingAsync(domainEvent.Order, cancellationToken);
    }
}

public class OrderShippedEventHandler : IDomainEventHandler<OrderShippedEvent>
{
    private readonly IEmailService _emailService;
    private readonly ILogger<OrderShippedEventHandler> _logger;
    
    public OrderShippedEventHandler(IEmailService emailService, ILogger<OrderShippedEventHandler> logger)
    {
        _emailService = emailService;
        _logger = logger;
    }
    
    public async Task HandleAsync(OrderShippedEvent domainEvent, CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Handling OrderShippedEvent for Order {OrderId}", domainEvent.Order.Id);
        
        await _emailService.SendShippingNotificationAsync(
            domainEvent.Order.CustomerId,
            domainEvent.Order.Id,
            domainEvent.TrackingNumber,
            cancellationToken);
    }
}

// MediatR integration for domain events (alternative approach)
public class MediatRDomainEventDispatcher : IDomainEventDispatcher
{
    private readonly IMediator _mediator;
    private readonly ILogger<MediatRDomainEventDispatcher> _logger;
    
    public MediatRDomainEventDispatcher(IMediator mediator, ILogger<MediatRDomainEventDispatcher> logger)
    {
        _mediator = mediator;
        _logger = logger;
    }
    
    public async Task DispatchAsync(DomainEvent domainEvent, CancellationToken cancellationToken = default)
    {
        _logger.LogDebug("Dispatching domain event: {EventType}", domainEvent.GetType().Name);
        await _mediator.Publish(domainEvent, cancellationToken);
    }
}

// Domain event notification with MediatR
public record OrderCreatedNotification : INotification
{
    public Order Order { get; }
    
    public OrderCreatedNotification(Order order)
    {
        Order = order;
    }
}

public class OrderCreatedNotificationHandler : INotificationHandler<OrderCreatedNotification>
{
    private readonly IEmailService _emailService;
    private readonly IInventoryService _inventoryService;
    
    public OrderCreatedNotificationHandler(IEmailService emailService, IInventoryService inventoryService)
    {
        _emailService = emailService;
        _inventoryService = inventoryService;
    }
    
    public async Task Handle(OrderCreatedNotification notification, CancellationToken cancellationToken)
    {
        await _inventoryService.ReserveStockAsync(notification.Order, cancellationToken);
        await _emailService.SendOrderConfirmationAsync(notification.Order.CustomerId, notification.Order.Id, cancellationToken);
    }
}

7. Factories for Complex Object Creation

Factories encapsulate complex creation logic, especially when object creation involves multiple steps or validation.

// Factory for creating Order aggregates
public interface IOrderFactory
{
    Order CreateOrder(int customerId, ShippingAddress shippingAddress, PaymentMethod paymentMethod);
    Order CreateExpressOrder(int customerId, ShippingAddress shippingAddress, PaymentMethod paymentMethod);
    Order CreateOrderWithGiftWrapping(int customerId, ShippingAddress shippingAddress, PaymentMethod paymentMethod, string giftMessage);
}

public class OrderFactory : IOrderFactory
{
    private readonly IProductRepository _productRepository;
    private readonly ILogger<OrderFactory> _logger;
    
    public OrderFactory(IProductRepository productRepository, ILogger<OrderFactory> logger)
    {
        _productRepository = productRepository;
        _logger = logger;
    }
    
    public Order CreateOrder(int customerId, ShippingAddress shippingAddress, PaymentMethod paymentMethod)
    {
        _logger.LogInformation("Creating order for customer {CustomerId}", customerId);
        return new Order(customerId, shippingAddress, paymentMethod);
    }
    
    public Order CreateExpressOrder(int customerId, ShippingAddress shippingAddress, PaymentMethod paymentMethod)
    {
        var order = new Order(customerId, shippingAddress, paymentMethod);
        // Express orders have higher priority
        // This could be implemented by adding a Priority property to Order
        return order;
    }
    
    public Order CreateOrderWithGiftWrapping(int customerId, ShippingAddress shippingAddress, 
        PaymentMethod paymentMethod, string giftMessage)
    {
        var order = new Order(customerId, shippingAddress, paymentMethod);
        // Add gift wrapping item
        order.AddItem(new Product(999, "Gift Wrapping", new Money(5.99m, "USD"), 100), 1);
        return order;
    }
}

// Factory with builder pattern for complex creation
public class OrderBuilder
{
    private int _customerId;
    private ShippingAddress _shippingAddress;
    private PaymentMethod _paymentMethod;
    private List<(int ProductId, int Quantity)> _items = new();
    private string _giftMessage;
    private bool _isExpress;
    private string _promoCode;
    
    public OrderBuilder ForCustomer(int customerId)
    {
        _customerId = customerId;
        return this;
    }
    
    public OrderBuilder ShipTo(ShippingAddress address)
    {
        _shippingAddress = address;
        return this;
    }
    
    public OrderBuilder PayWith(PaymentMethod method)
    {
        _paymentMethod = method;
        return this;
    }
    
    public OrderBuilder AddItem(int productId, int quantity)
    {
        _items.Add((productId, quantity));
        return this;
    }
    
    public OrderBuilder WithGiftMessage(string message)
    {
        _giftMessage = message;
        return this;
    }
    
    public OrderBuilder ExpressDelivery()
    {
        _isExpress = true;
        return this;
    }
    
    public OrderBuilder ApplyPromoCode(string code)
    {
        _promoCode = code;
        return this;
    }
    
    public async Task<Order> BuildAsync(IProductRepository productRepository)
    {
        var order = new Order(_customerId, _shippingAddress, _paymentMethod);
        
        foreach (var (productId, quantity) in _items)
        {
            var product = await productRepository.GetByIdAsync(productId);
            if (product == null)
                throw new BusinessException($"Product {productId} not found");
                
            order.AddItem(product, quantity);
        }
        
        if (!string.IsNullOrEmpty(_giftMessage))
        {
            order.AddItem(new Product(999, "Gift Wrapping", new Money(5.99m, "USD"), 100), 1);
        }
        
        // Apply promo code discount
        if (!string.IsNullOrEmpty(_promoCode))
        {
            // Apply discount logic
        }
        
        return order;
    }
}

// Usage of factory with builder
public class OrderServiceWithFactory
{
    private readonly IOrderFactory _orderFactory;
    private readonly IOrderRepository _orderRepository;
    private readonly IProductRepository _productRepository;
    
    public OrderServiceWithFactory(
        IOrderFactory orderFactory,
        IOrderRepository orderRepository,
        IProductRepository productRepository)
    {
        _orderFactory = orderFactory;
        _orderRepository = orderRepository;
        _productRepository = productRepository;
    }
    
    public async Task<Order> CreateComplexOrderAsync(int customerId, CreateOrderRequest request)
    {
        var order = await new OrderBuilder()
            .ForCustomer(customerId)
            .ShipTo(request.ShippingAddress)
            .PayWith(request.PaymentMethod)
            .AddItem(request.ProductId, request.Quantity)
            .WithGiftMessage(request.GiftMessage)
            .ExpressDelivery()
            .ApplyPromoCode(request.PromoCode)
            .BuildAsync(_productRepository);
            
        await _orderRepository.AddAsync(order);
        
        return order;
    }
}

8. Specification Pattern for Business Rules

Specifications encapsulate business rules in a reusable, composable way.

// Base specification
public abstract class Specification<T>
{
    public abstract Expression<Func<T, bool>> ToExpression();
    
    public bool IsSatisfiedBy(T entity)
    {
        var predicate = ToExpression().Compile();
        return predicate(entity);
    }
    
    public Specification<T> And(Specification<T> specification)
    {
        return new AndSpecification<T>(this, specification);
    }
    
    public Specification<T> Or(Specification<T> specification)
    {
        return new OrSpecification<T>(this, specification);
    }
    
    public Specification<T> Not()
    {
        return new NotSpecification<T>(this);
    }
}

// Composite specifications
public class AndSpecification<T> : Specification<T>
{
    private readonly Specification<T> _left;
    private readonly Specification<T> _right;
    
    public AndSpecification(Specification<T> left, Specification<T> right)
    {
        _left = left;
        _right = right;
    }
    
    public override Expression<Func<T, bool>> ToExpression()
    {
        var leftExpr = _left.ToExpression();
        var rightExpr = _right.ToExpression();
        
        var parameter = Expression.Parameter(typeof(T));
        var leftInvoke = Expression.Invoke(leftExpr, parameter);
        var rightInvoke = Expression.Invoke(rightExpr, parameter);
        var andExpression = Expression.AndAlso(leftInvoke, rightInvoke);
        
        return Expression.Lambda<Func<T, bool>>(andExpression, parameter);
    }
}

public class OrSpecification<T> : Specification<T>
{
    private readonly Specification<T> _left;
    private readonly Specification<T> _right;
    
    public OrSpecification(Specification<T> left, Specification<T> right)
    {
        _left = left;
        _right = right;
    }
    
    public override Expression<Func<T, bool>> ToExpression()
    {
        var leftExpr = _left.ToExpression();
        var rightExpr = _right.ToExpression();
        
        var parameter = Expression.Parameter(typeof(T));
        var leftInvoke = Expression.Invoke(leftExpr, parameter);
        var rightInvoke = Expression.Invoke(rightExpr, parameter);
        var orExpression = Expression.OrElse(leftInvoke, rightInvoke);
        
        return Expression.Lambda<Func<T, bool>>(orExpression, parameter);
    }
}

public class NotSpecification<T> : Specification<T>
{
    private readonly Specification<T> _specification;
    
    public NotSpecification(Specification<T> specification)
    {
        _specification = specification;
    }
    
    public override Expression<Func<T, bool>> ToExpression()
    {
        var expr = _specification.ToExpression();
        var parameter = expr.Parameters[0];
        var notExpression = Expression.Not(expr.Body);
        
        return Expression.Lambda<Func<T, bool>>(notExpression, parameter);
    }
}

// Concrete specifications for Order
public class ActiveOrderSpecification : Specification<Order>
{
    public override Expression<Func<Order, bool>> ToExpression()
    {
        return order => order.Status != OrderStatus.Cancelled &&
                       order.Status != OrderStatus.Delivered;
    }
}

public class HighValueOrderSpecification : Specification<Order>
{
    private readonly decimal _minimumAmount;
    
    public HighValueOrderSpecification(decimal minimumAmount)
    {
        _minimumAmount = minimumAmount;
    }
    
    public override Expression<Func<Order, bool>> ToExpression()
    {
        return order => order.TotalAmount.Amount >= _minimumAmount;
    }
}

public class OrderByCustomerSpecification : Specification<Order>
{
    private readonly int _customerId;
    
    public OrderByCustomerSpecification(int customerId)
    {
        _customerId = customerId;
    }
    
    public override Expression<Func<Order, bool>> ToExpression()
    {
        return order => order.CustomerId == _customerId;
    }
}

public class RecentOrderSpecification : Specification<Order>
{
    private readonly TimeSpan _recentPeriod;
    
    public RecentOrderSpecification(TimeSpan recentPeriod)
    {
        _recentPeriod = recentPeriod;
    }
    
    public override Expression<Func<Order, bool>> ToExpression()
    {
        var cutoffDate = DateTime.UtcNow.Subtract(_recentPeriod);
        return order => order.CreatedAt >= cutoffDate;
    }
}

// Using specifications
public class OrderQueryService
{
    private readonly IOrderRepository _orderRepository;
    
    public OrderQueryService(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }
    
    public async Task<IEnumerable<Order>> GetVIPCustomerOrdersAsync(int customerId)
    {
        // Combine specifications
        var customerSpec = new OrderByCustomerSpecification(customerId);
        var activeSpec = new ActiveOrderSpecification();
        var highValueSpec = new HighValueOrderSpecification(1000);
        
        // VIP orders: active AND high-value
        var vipSpec = customerSpec.And(activeSpec).And(highValueSpec);
        
        return await _orderRepository.FindAsync(vipSpec);
    }
    
    public async Task<IEnumerable<Order>> GetRecentHighValueOrdersAsync()
    {
        var recentSpec = new RecentOrderSpecification(TimeSpan.FromDays(30));
        var highValueSpec = new HighValueOrderSpecification(5000);
        
        var spec = recentSpec.And(highValueSpec);
        
        return await _orderRepository.FindAsync(spec);
    }
}

9. Anti-Corruption Layer

The Anti-Corruption Layer protects the domain model from external systems (legacy systems, third-party APIs).

// External legacy system
public class LegacyOrderSystem
{
    public LegacyOrder GetOrder(string orderId)
    {
        // Returns legacy data structure
        return new LegacyOrder
        {
            OrderId = orderId,
            CustomerCode = "CUST001",
            OrderDate = "2024-01-15",
            Total = "299.99",
            Status = "P",
            Items = new List<LegacyOrderItem>()
        };
    }
    
    public void UpdateOrderStatus(string orderId, string status)
    {
        // Update legacy system
    }
}

// Legacy data structures
public class LegacyOrder
{
    public string OrderId { get; set; }
    public string CustomerCode { get; set; }
    public string OrderDate { get; set; }
    public string Total { get; set; }
    public string Status { get; set; }
    public List<LegacyOrderItem> Items { get; set; }
}

public class LegacyOrderItem
{
    public string ProductCode { get; set; }
    public string Quantity { get; set; }
    public string Price { get; set; }
}

// Anti-Corruption Layer - Translator
public class LegacyOrderTranslator
{
    private readonly ICustomerRepository _customerRepository;
    private readonly IProductRepository _productRepository;
    
    public LegacyOrderTranslator(ICustomerRepository customerRepository, IProductRepository productRepository)
    {
        _customerRepository = customerRepository;
        _productRepository = productRepository;
    }
    
    public async Task<Order> TranslateToDomainAsync(LegacyOrder legacyOrder)
    {
        // Translate customer code to domain customer
        var customer = await _customerRepository.GetByCodeAsync(legacyOrder.CustomerCode);
        
        // Translate status
        var status = TranslateStatus(legacyOrder.Status);
        
        // Create domain order
        var order = new Order(customer.Id, new ShippingAddress("", "", "", "", ""), PaymentMethod.CreditCard);
        
        // Translate items
        foreach (var legacyItem in legacyOrder.Items)
        {
            var product = await _productRepository.GetByCodeAsync(legacyItem.ProductCode);
            var quantity = int.Parse(legacyItem.Quantity);
            order.AddItem(product, quantity);
        }
        
        return order;
    }
    
    private OrderStatus TranslateStatus(string legacyStatus)
    {
        return legacyStatus switch
        {
            "P" => OrderStatus.Pending,
            "C" => OrderStatus.Confirmed,
            "S" => OrderStatus.Shipped,
            "D" => OrderStatus.Delivered,
            "X" => OrderStatus.Cancelled,
            _ => OrderStatus.Pending
        };
    }
}

// Anti-Corruption Layer - Facade
public interface ILegacyOrderFacade
{
    Task<Order> GetOrderAsync(string orderId);
    Task UpdateOrderStatusAsync(string orderId, OrderStatus status);
}

public class LegacyOrderFacade : ILegacyOrderFacade
{
    private readonly LegacyOrderSystem _legacySystem;
    private readonly LegacyOrderTranslator _translator;
    private readonly ILogger<LegacyOrderFacade> _logger;
    
    public LegacyOrderFacade(
        LegacyOrderSystem legacySystem,
        LegacyOrderTranslator translator,
        ILogger<LegacyOrderFacade> logger)
    {
        _legacySystem = legacySystem;
        _translator = translator;
        _logger = logger;
    }
    
    public async Task<Order> GetOrderAsync(string orderId)
    {
        _logger.LogInformation("Fetching order {OrderId} from legacy system", orderId);
        
        var legacyOrder = await Task.Run(() => _legacySystem.GetOrder(orderId));
        var domainOrder = await _translator.TranslateToDomainAsync(legacyOrder);
        
        _logger.LogInformation("Successfully translated order {OrderId}", orderId);
        
        return domainOrder;
    }
    
    public async Task UpdateOrderStatusAsync(string orderId, OrderStatus status)
    {
        var legacyStatus = TranslateToLegacyStatus(status);
        
        _logger.LogInformation("Updating order {OrderId} status to {Status} in legacy system", orderId, legacyStatus);
        
        await Task.Run(() => _legacySystem.UpdateOrderStatus(orderId, legacyStatus));
    }
    
    private string TranslateToLegacyStatus(OrderStatus status)
    {
        return status switch
        {
            OrderStatus.Pending => "P",
            OrderStatus.Confirmed => "C",
            OrderStatus.Shipped => "S",
            OrderStatus.Delivered => "D",
            OrderStatus.Cancelled => "X",
            _ => "P"
        };
    }
}

10. Summary and Key Takeaways

  • Ubiquitous Language: Use the same language in code as business experts use in conversation
  • Entities: Have identity, mutable, lifecycle matters
  • Value Objects: No identity, immutable, defined by attributes
  • Aggregates: Consistency boundaries, access only through aggregate root
  • Repositories: Persistence abstraction for aggregates
  • Domain Services: Business logic that doesn't fit in entities
  • Domain Events: Capture important business occurrences, enable loose coupling
  • Factories: Encapsulate complex object creation
  • Specifications: Encapsulate business rules in reusable components
  • Anti-Corruption Layer: Protect domain from external systems

🎯 DDD Best Practices:

  • Start with the business domain, not the database
  • Keep the domain model persistent-ignorant
  • Use factories for complex object creation
  • Use specifications for business rules
  • Use domain events to decouple side effects
  • Keep aggregates small and focused
  • Validate invariants within the aggregate
  • Use value objects to encapsulate concepts

11. Practice Exercises

Exercise 1: Design a Banking Account Aggregate

Create an Account aggregate with proper invariants, including deposit, withdrawal, and transfer operations.

Click to see solution
public class Account : IAggregateRoot
{
    private readonly List<Transaction> _transactions = new();
    
    public int Id { get; private set; }
    public string AccountNumber { get; private set; }
    public int CustomerId { get; private set; }
    public Money Balance { get; private set; }
    public AccountStatus Status { get; private set; }
    public DateTime CreatedAt { get; private set; }
    public DateTime? ClosedAt { get; private set; }
    
    public IReadOnlyCollection<Transaction> Transactions => _transactions.AsReadOnly();
    
    private Account() { }
    
    public Account(int customerId, string accountNumber, Money initialDeposit)
    {
        if (initialDeposit.Amount < 0)
            throw new BusinessException("Initial deposit cannot be negative");
            
        CustomerId = customerId;
        AccountNumber = accountNumber;
        Balance = initialDeposit;
        Status = AccountStatus.Active;
        CreatedAt = DateTime.UtcNow;
        
        if (initialDeposit.Amount > 0)
        {
            _transactions.Add(new Transaction(TransactionType.Deposit, initialDeposit, "Initial deposit"));
        }
    }
    
    public void Deposit(Money amount, string reference)
    {
        if (Status != AccountStatus.Active)
            throw new BusinessException("Cannot deposit to inactive account");
            
        if (amount.Amount <= 0)
            throw new BusinessException("Deposit amount must be positive");
            
        Balance = Balance.Add(amount);
        _transactions.Add(new Transaction(TransactionType.Deposit, amount, reference));
    }
    
    public WithdrawalResult Withdraw(Money amount, string reference)
    {
        if (Status != AccountStatus.Active)
            throw new BusinessException("Cannot withdraw from inactive account");
            
        if (amount.Amount <= 0)
            throw new BusinessException("Withdrawal amount must be positive");
            
        if (Balance.Amount < amount.Amount)
            throw new BusinessException("Insufficient funds");
            
        Balance = Balance.Subtract(amount);
        _transactions.Add(new Transaction(TransactionType.Withdrawal, amount, reference));
        
        return new WithdrawalResult(amount, reference);
    }
    
    public void Close()
    {
        if (Status == AccountStatus.Closed)
            throw new BusinessException("Account is already closed");
            
        if (Balance.Amount > 0)
            throw new BusinessException("Cannot close account with positive balance");
            
        Status = AccountStatus.Closed;
        ClosedAt = DateTime.UtcNow;
    }
}

Exercise 2: Implement Domain Events for Order Processing

Create domain events and handlers for order status changes.

Click to see solution
public class OrderStatusChangedEvent : DomainEvent
{
    public int OrderId { get; }
    public OrderStatus OldStatus { get; }
    public OrderStatus NewStatus { get; }
    
    public OrderStatusChangedEvent(int orderId, OrderStatus oldStatus, OrderStatus newStatus)
    {
        OrderId = orderId;
        OldStatus = oldStatus;
        NewStatus = newStatus;
    }
}

public class SendEmailOnOrderStatusChangedHandler : IDomainEventHandler<OrderStatusChangedEvent>
{
    private readonly IEmailService _emailService;
    private readonly IOrderRepository _orderRepository;
    
    public SendEmailOnOrderStatusChangedHandler(
        IEmailService emailService,
        IOrderRepository orderRepository)
    {
        _emailService = emailService;
        _orderRepository = orderRepository;
    }
    
    public async Task HandleAsync(OrderStatusChangedEvent domainEvent, CancellationToken cancellationToken)
    {
        var order = await _orderRepository.GetByIdAsync(domainEvent.OrderId, cancellationToken);
        if (order == null) return;
        
        var subject = $"Order #{order.OrderNumber} Status Update";
        var body = $"Your order status has changed from {domainEvent.OldStatus} to {domainEvent.NewStatus}";
        
        await _emailService.SendAsync(order.CustomerId, subject, body, cancellationToken);
    }
}

12. What's Next?

In Chapter 8, we'll put all our knowledge into practice with a comprehensive Case Study - Parking Lot System. We'll implement a complete LLD solution using all the patterns and principles we've learned so far.

Chapter 8 Preview:

  • Requirements Analysis - Understanding the problem domain
  • Domain Modeling - Identifying entities, value objects, aggregates
  • Design Patterns in Action - Strategy, State, Observer, Factory
  • Complete Implementation - Full working code with tests
  • API Design - REST endpoints for parking operations

📝 Practice Assignments:

  1. Create an e-commerce domain with Order, Product, and Customer aggregates
  2. Implement value objects for Money, Email, and Address with validation
  3. Design a banking domain with Account, Transaction, and TransferService
  4. Implement domain events for order lifecycle with multiple handlers
  5. Create specifications for filtering orders by status, date range, and amount
  6. Build an anti-corruption layer for integrating with an external inventory system
  7. Design a factory for creating different types of orders (express, gift-wrapped, etc.)

Happy Coding! 🚀

إرسال تعليق

Feel free to ask your query...
Cookie Consent
We serve cookies on this site to analyze traffic, remember your preferences, and optimize your experience.
Oops!
It seems there is something wrong with your internet connection. Please connect to the internet and start browsing again.
AdBlock Detected!
We have detected that you are using adblocking plugin in your browser.
The revenue we earn by the advertisements is used to manage this website, we request you to whitelist our website in your adblocking plugin.
Site is Blocked
Sorry! This site is not available in your country.