Domain-Driven Design in Practice - Building Better Software

#DDD#DomainDrivenDesign#SoftwareArchitecture#CleanArchitecture#EnterpriseArchitecture#SandyTech#KothapalliSandeep#IdeaToMVP

Domain-Driven Design in Practice - Building Better Software

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.

What is Domain-Driven Design?

DDD is a methodology that connects implementation to an evolving model. It emphasizes:

  • Ubiquitous Language: A common language used by developers and domain experts
  • Bounded Contexts: Explicit boundaries within which a domain model applies
  • Entities and Value Objects: Building blocks of domain models
  • Aggregates: Clusters of entities treated as a single unit
  • Domain Services: Operations that don't naturally fit within entities

Core Concepts

Ubiquitous Language

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;
    }
}

Bounded Contexts

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 vs Value Objects

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

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

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");
    }
}

Layered Architecture

DDD typically uses a layered architecture:

┌─────────────────────────────────┐
│   Presentation Layer (UI/API)  │
├─────────────────────────────────┤
│   Application Layer (Use Cases)│
├─────────────────────────────────┤
│   Domain Layer (Business Logic) │
├─────────────────────────────────┤
│   Infrastructure Layer (Data)   │
└─────────────────────────────────┘

Application Layer

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;
    }
}

Implementing DDD in .NET

Project Structure

OrderManagement.Domain/
  ├── Entities/
  ├── ValueObjects/
  ├── Aggregates/
  ├── DomainServices/
  └── DomainEvents/

OrderManagement.Application/
  ├── Commands/
  ├── Queries/
  └── DTOs/

OrderManagement.Infrastructure/
  ├── Persistence/
  └── ExternalServices/

OrderManagement.API/
  ├── Controllers/
  └── Program.cs

Domain Events

public abstract class DomainEvent
{
    public DateTime OccurredOn { get; } = DateTime.UtcNow;
}
 
public class OrderConfirmedEvent : DomainEvent
{
    public OrderId OrderId { get; }
    
    public OrderConfirmedEvent(OrderId orderId)
    {
        OrderId = orderId;
    }
}

Best Practices

  1. Start with the Domain: Focus on business logic first
  2. Use Ubiquitous Language: Ensure code reflects business terminology
  3. Keep Aggregates Small: Large aggregates lead to performance and concurrency issues
  4. Protect Invariants: Ensure business rules are always enforced
  5. Use Value Objects: Prefer value objects for concepts without identity
  6. Domain Events: Use events for cross-aggregate communication
  7. Repository Pattern: Abstract data access from domain logic

Common Pitfalls

  • Anemic Domain Model: Entities with only getters/setters
  • God Objects: Aggregates that are too large
  • Leaky Abstractions: Infrastructure concerns leaking into domain
  • Over-Engineering: Applying DDD where simpler approaches would work

When to Use DDD

DDD is most valuable when:

  • Domain logic is complex
  • Business rules are intricate
  • Long-term maintainability is critical
  • Team includes domain experts
  • Application will evolve significantly

Conclusion

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.