Post

Comprehensive Guide to Domain-Driven Design (DDD)

Domain-Driven Design (DDD) is a strategic approach to software development that centers on building a deep understanding of the business domain. It provides tools to manage complexity by aligning the software model with business concepts. In this post, we will explore various advanced concepts of DDD, providing examples for each, and examine how they can be applied in .NET projects.

Core Concepts of DDD

1. Ubiquitous Language

The Ubiquitous Language is a shared vocabulary used by both developers and domain experts (i.e Producy Owner). It ensures that all parties involved in the development process are on the same page, making communication more efficient. For example:

  • Customer means the same thing to both developers and business users.
  • Order Status has the same life cycle across the application: Pending, Shipped, Delivered.

2. Entities

Objects that have a distinct identity, such as OrderItem, which persist over time.

1
2
3
4
5
public record OrderItem(
    Guid Id,
    string ProductName,
    decimal Price,
    int Quantity);

3. Value Objects

Immutable objects that are defined by their properties rather than a unique identity. Common examples include Address or Price, which are considered equal if all their properties match. Unlike entities, Value Objects do not have an Id and focus on structural equality rather than identity.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public record Price
{
    public decimal Value { get; init; }

    private Price(decimal value)
    {
        if (value < 0)
        {
            throw new ArgumentException("Price cannot be negative", nameof(value));
        }

        Value = value;
    }

    public static Price Create(decimal value) =>
        new(value);

    // Operator overloading to perform operations with prices
    public static Price operator +(Price a, Price b) => new Price(a.Value + b.Value);
    public static Price operator -(Price a, Price b) => new Price(a.Value - b.Value);

    public override string ToString() => Value.ToString();
}

4. Aggregates

An Aggregate is a cluster of related objects treated as a single unit.
Example of an Order aggregate with OrderItems:

1
2
3
4
5
public record Order(
    Guid Id,
    DateTime OrderDate,
    Customer Customer,
    List<OrderItem> Items);

5. Repositories

A Repository acts as a collection of aggregate roots and provides an abstraction over data access. It isolates the domain model from the persistence layer.

1
2
3
4
5
6
public class IOrderRepository
{
    Task<Order> GetById(int id);

    Task Save(Order order);
}

6. Domain Services

Domain Services encapsulate domain logic that doesn’t naturally belong to an entity or value object, often because they span across multiple aggregates.

1
2
3
4
5
6
7
public class PaymentService
{
    public bool ProcessPayment(Order order, PaymentDetails paymentDetails)
    {
        // Domain logic to process payment
    }
}

Advanced DDD Concepts

1. Bounded Contexts

A Bounded Context defines clear boundaries around a specific domain model, preventing ambiguities and inconsistencies across the system. Each context focuses on a particular aspect of the business, ensuring models don’t overlap.

For example, the Order aggregate in the Sales Context might contain customer and payment details.

1
2
3
4
5
6
public record Order(
    Guid Id,
    DateTime OrderDate,
    Customer Customer,
    List<OrderItem> Items,
    Payment Payment);

While the Order aggregate in the Shipping Context might only include shipping-related information.

1
2
3
4
5
6
public record Order(
    Guid Id,
    DateTime OrderDate,
    List<OrderItem> Items,
    ShippingAddress ShippingAddress,
    string TrackingNumber);

2. Domain Events

Domain events notify the system of significant changes in the business logic. They are used to trigger actions in other parts of the application without creating tight coupling.

1
2
3
public record OrderPlacedEvent(
    int OrderId,
    DateTime PlacedOn) : IDomainEvent;
1
2
3
4
5
6
7
public class OrderPlacedHandler : IEventHandler<OrderPlacedEvent>
{
    public Task Handle(OrderPlacedEvent domainEvent)
    {
        // Logic to handle the order placed event
    }
}

3. Factories

A Factory is used to encapsulate the logic of creating complex aggregates. It ensures that aggregates are constructed following all domain rules.

1
2
3
4
5
6
7
8
9
10
11
12
public class OrderFactory
{
    public static Order CreateOrder(Customer customer, List<OrderItem> items)
    {
        var order = new Order(customer.Id);
        foreach (var item in items)
        {
            order.AddItem(item);
        }
        return order;
    }
}

4. Event Sourcing

With Event Sourcing, the state of an aggregate is represented as a series of domain events. Instead of storing the current state directly, the system maintains a log of all the events that have led to the current state. The current state is reconstructed by replaying these events.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Order
{
    public OrderStatus Status { Get; Set; }
    private List<IDomainEvent> _events = new List<IDomainEvent>();

    public void Apply(OrderPlacedEvent @event)
    {
        // Update the internal state based on the event
        this.Status = OrderStatus.Placed;

        // Store the event for later persistence
        _events.Add(@event);
    }

    public IEnumerable<IDomainEvent> GetUncommittedEvents()
    {
        return _events;
    }
}

Key Points:

  • Event Storage: Those events should be persisted somewhere, like databases, blob storage, or event stores. This allows for future reconstruction of the state and enables features like auditing and temporal queries.

  • Replaying Events: To reconstruct the state of an aggregate, you can replay the stored events. This means that your domain events must be immutable and represent state changes effectively.

  • Handling Uncommitted Events: It’s often useful to have a method to clear uncommitted events after they have been persisted to avoid duplicating them.

5. Anti-Corruption Layer (ACL)

The Anti-Corruption Layer is a boundary that protects the internal model of a bounded context from being polluted by external systems. It translates external models into the domain’s models.

1
2
3
4
5
6
7
public class ExternalCustomerAdapter
{
    public Customer Convert(ExternalCustomer externalCustomer)
    {
        // Logic to map external customer to domain customer
    }
}

You can use external mappers like Mapster, AutoMapper,…

6. Specifications

A Specification is a pattern used to encapsulate business rules into reusable objects. It can be used to validate objects or filter collections based on specific criteria.

1
2
3
4
5
6
7
public class EligibleForDiscountSpecification : ISpecification<Order>
{
    public bool IsSatisfiedBy(Order order)
    {
        return order.TotalAmount > 100;
    }
}
1
2
3
4
if (discountSpec.IsSatisfiedBy(order))
{
    Console.WriteLine("Order is eligible for a discount.");
}

7. Policies

A Policy encapsulates complex business rules that might affect different parts of the system. Policies can be executed in response to domain events or when certain conditions in the domain are met.

1
2
3
4
5
6
7
8
9
10
11
public class DiscountPolicy
{
    public decimal ApplyDiscount(Order order)
    {
        if (order.TotalAmount > 500)
        {
            return order.TotalAmount * 0.10m; // 10% discount
        }
        return 0;
    }
}

Benefits of DDD

  • Clarity: DDD aligns the domain model with the business, making the software design intuitive.
  • Scalability: By splitting large domains into bounded contexts and leveraging CQRS and Event Sourcing, DDD allows the system to scale both in terms of complexity and performance.
  • Flexibility: Changes in the business domain are easier to manage and reflect in the software.

Challenges of DDD

  • Learning Curve: Understanding and applying DDD can be difficult for teams unfamiliar with the patterns and concepts.
  • Complexity: DDD introduces additional complexity in terms of layers and abstractions, which can be unnecessary for smaller or less complex domains.

Relationship Between DDD, Clean Architecture, and CQRS

Domain-Driven Design (DDD), Clean Architecture, and CQRS (Command Query Responsibility Segregation) are complementary approaches that help create scalable, maintainable, and well-structured software systems.

  • DDD focuses on modeling the business domain, ensuring that the core logic reflects real-world processes. It emphasizes concepts like Aggregates, Entities, and Value Objects, while encouraging collaboration between developers and domain experts.

  • Clean Architecture, championed by Robert C. Martin, emphasizes a separation of concerns by organizing code into layers, ensuring the business logic is independent of frameworks, UI, and databases. This structure aligns well with DDD by keeping the domain model at the center of the architecture, isolated from external dependencies.

  • CQRS fits naturally into DDD and Clean Architecture, as it separates the responsibility of commands (writing data) from queries (reading data). This segregation enables a more focused design for complex business operations, allowing different models for reading and writing, which complements DDD’s Bounded Contexts and Clean Architecture’s modular approach.

I will cover these topics in more detail in other posts, providing a deeper understanding of how they interconnect and can be effectively implemented in software projects.

Conclusion

Domain-Driven Design offers a systematic approach to managing complexity in software development by aligning the design with business concepts. While it introduces complexity, especially for new teams, the long-term benefits in terms of scalability, maintainability, and business alignment make it an excellent choice for large and intricate domains.

Want more?

Dive into Eric Evans’ groundbreaking book: Domain-Driven Design: Tackling Complexity in the Heart of Software. Published in 2003, this seminal work lays the foundation for the DDD methodology, offering a comprehensive guide to managing complexity in software systems. By focusing on the core business domain and fostering collaboration between developers and domain experts, Evans provides the blueprint for building more maintainable and scalable software solutions.

This post is licensed under CC BY 4.0 by the author.