Understanding of Domain Driven Design (DDD)

Introduction

A software development approach that helps to model complex software that closely aligns with the business domain according to the business needs. If you are interested in learning more about (DDD) Domain Driven Design in depth check out Eric Even's book “Domain-Driven Design: Tackling Complexity in the Heart of Software“.

Domain Driven Design

Core Concepts of DDD

  • Entities and Value Objects
  • Aggregate and Aggregate Roots
  • Ubiquitous Language
  • Bounded Context
  • Domain Services
  • Domain Events

We will talk about the first two of them in this article.

Entities and Value Objects

An entity represents a specific part of a business domain and can be thought of as a model or a table in database terms. It also represents an object that has behaviors according to its use case. An entity is typically identified by a unique identifier (such as a primary key) that distinguishes it across different states.

A value object is an object without an identity, meaning it is defined entirely by its attributes and not by a unique identifier. It becomes part of an entity to provide more context or meaning within that entity. In a multi-threaded environment, value objects are particularly useful because they are immutable, meaning once they are created, they cannot be altered.

What is the use of Value Object in DDD?

For example, when an order is placed for a product, the price of the product at the time of the order should be captured as a value object, like Money. Once the Money object is created, its value should not change, ensuring consistency even if the product's price changes later. This immutability prevents potential inconsistencies or errors that could arise if, for instance, the price was accidentally modified by another thread or part of the code. By using a value object, you ensure that the price associated with the order remains the same, regardless of any future changes to the product's price.

Example of creating value object.

public class Product
{
    public int Id { get; private set; }
    public string Name { get; private set; }
    public Money Price { get; private set; }
    public Product(int id, string name, Money price)
    {
        if (string.IsNullOrEmpty(name)) throw new ArgumentException("Product name is required.");
        Id = id;
        Name = name;
        Price = price ?? throw new ArgumentNullException(nameof(price));
    }
}

Aggregate and Aggregate Roots

An aggregate refers to a collection of related entities that together form a cohesive unit. For example, in a domain model, an Order and its associated OrderItem entities are closely related and can be combined to create an aggregate. The Aggregate Root is the primary entity through which the aggregate is accessed and managed; in this case, Order serves as the Aggregate Root.

Aggregating related entities helps maintain the integrity of the entire business use case by ensuring that operations are performed within a consistent context. It also facilitates loading all relevant entities into memory together, which simplifies operations and enforces business rules across the aggregate.

The main benefit of defining the aggregate is to enforce business rules on the whole cluster of entities, and also, you can handle the domain events related to the aggregate inside the aggregate. We will talk about the domain events later.

public class Order
{
    private const int MaxItems = 10;
    public int Id { get; private set; }
    public List<OrderItem> Items { get; private set; } = new List<OrderItem>();
    public Order(int id)
    {
        Id = id;
    }
    public void AddItem(OrderItem item)
    {
        if (Items.Count >= MaxItems)
            throw new InvalidOperationException("Cannot add more items; maximum limit reached.");

        Items.Add(item);
    }
    public decimal TotalPrice => Items.Sum(item => item.Price * item.Quantity);
}
public class OrderItem
{
    public int ProductId { get; private set; }
    public decimal Price { get; private set; }
    public int Quantity { get; private set; }
    public OrderItem(int productId, decimal price, int quantity)
    {
        ProductId = productId;
        Price = price;
        Quantity = quantity;
    }
}

You can also create a separate Aggregate Root class and move validations roles inside it instead of in the Order class to make the Order class much cleaner. Then, Inherit the Order class from the Aggregate class.  I prefer this method, but the above is the simplest example of understanding the aggregate.

What’s the benefit of using it in the code technically?

  • Aggregate ensures that related entities are modified consistently, maintaining business rules and integrity.
  • It centralizes logic and rules within the aggregate root, reducing the risk of errors and simplifying maintenance.
  • Operations within the aggregate are atomic, meaning all changes are applied together, ensuring data consistency.

However, loading large aggregates and performing operations on them can be expensive, and performance issues can occur. We will talk about this in the next blog to address this issue.

In the next blog, we will talk about the next two.

  • Ubiquitous Language
  • Bounded Context

That's it for today. Thank you for reading.


Similar Articles