DDD: Entity vs. Value Object vs. Aggregate Root

DDD: Entity vs. Value Object vs. Aggregate Root

·

8 min read

In the realm of Domain-Driven Design (DDD), developers often encounter the concepts of Entity, Value Object, and Aggregate Root. These building blocks play a crucial role in modeling complex domains and ensuring a clear and maintainable structure for your software applications. In a previous article, we delved into the distinction between Aggregates and Aggregate Roots. Now, let's dive into the finer points of these entities, value objects, and aggregate roots.

Recap: The Difference Between Aggregates and Aggregate Roots

Before we delve into the specifics of Entity and Value Object, let's briefly recap the distinction between Aggregates and Aggregate Roots. In a nutshell, an Aggregate is a cluster of domain objects that are treated as a single unit for data changes. The Aggregate Root is the primary access point to this cluster, maintaining consistency and encapsulating the internal objects. To understand this concept better, you can refer to our previous article here.

Entities: Identity and Mutability

An Entity represents a distinct object with a unique identity that remains consistent throughout its lifecycle. This identity enables us to track and differentiate between different instances of the same entity. Typically, entities are mutable; they can change attributes while retaining their identity. Let's look at an example in C#:

public class Order
{
    public Guid Id { get; set; }
    public string CustomerName { get; set; }
    public DateTime OrderDate { get; set; }
    // Other properties and methods...
}

In this example, the Order class is an entity. Each order has a unique Id that distinguishes it from other orders, and its attributes like CustomerName and OrderDate can be modified without changing its identity.

Value Objects: Immutability and Equality

A Value Object represents a concept in the domain with no distinct identity; it is defined by its attributes rather than an identifier. Value objects are immutable, meaning they cannot be changed once created. They are ideal for representing components of an entity's attributes. Let's see a C# example:

public class Address
{
    public string Street { get; }
    public string City { get; }
    public string ZipCode { get; }

    public Address(string street, string city, string zipCode)
    {
        Street = street;
        City = city;
        ZipCode = zipCode;
    }

    // Equality and other methods...
}

In this case, the Address class is a value object. It represents an address with its attributes, and once an address is created, its values remain constant. Value objects are compared by their attributes rather than an identity.

Aggregate Roots: Controlling Access and Consistency

The Aggregate Root serves as the entry point for accessing the objects within an aggregate. It maintains consistency and enforces business rules within the aggregate. An aggregate root can consist of entities and value objects. Let's build upon our previous example:

public class Customer : AggregateRoot
{
    public Guid Id { get; private set; }
    public string Name { get; private set; }
    private List<Order> Orders { get; set; }

    public Customer(string name)
    {
        Id = Guid.NewGuid();
        Name = name;
        Orders = new List<Order>();
    }

    public void PlaceOrder(DateTime orderDate)
    {
        var order = new Order { Id = Guid.NewGuid(), CustomerName = Name, OrderDate = orderDate };
        Orders.Add(order);
    }

    // Other methods...
}

Here, the Customer class is an aggregate root that encapsulates the Order entities. It controls the consistency and access to orders placed by a customer.

A Real-World Example

To solidify our understanding of Entity, Value Object, and Aggregate Root, let's consider a real-world example of an e-commerce application in C#. This example will showcase how these concepts work together to model a complex domain.

The E-Commerce Domain

Imagine we're building an e-commerce platform. In this domain, we have two main entities: Product and Customer. Additionally, we have a value object called Address.

Product Entity

public class Product : Entity
{
    public int Id { get; private set; }
    public string Name { get; private set; }
    public decimal Price { get; private set; }

    public Product(string name, decimal price)
    {
        Name = name;
        Price = price;
    }

    // Other methods...
}

In this example, the Product class represents an entity. Each product has a unique Id and attributes like Name and Price. Products are distinguishable by their identity, and their attributes can be modified.

Customer Entity and Address Value Object

public class Customer : AggregateRoot
{
    public Guid Id { get; private set; }
    public string Name { get; private set; }
    private List<Order> Orders { get; set; }
    public Address ShippingAddress { get; private set; }

    public Customer(string name, Address shippingAddress)
    {
        Id = Guid.NewGuid();
        Name = name;
        ShippingAddress = shippingAddress;
        Orders = new List<Order>();
    }

    public void PlaceOrder(List<Product> products)
    {
        var order = new Order(products, ShippingAddress);
        Orders.Add(order);
    }

    // Other methods...
}

public class Address : ValueObject
{
    public string Street { get; }
    public string City { get; }
    public string ZipCode { get; }

    public Address(string street, string city, string zipCode)
    {
        Street = street;
        City = city;
        ZipCode = zipCode;
    }

    // Equality and other methods...
}

In this example, the Customer class acts as an aggregate root. It encapsulates orders and holds an instance of the Address value object for shipping purposes. The Address class is a value object, representing the customer's address without an identity.

Putting It All Together

With our entities, value object, and aggregate root in place, we can create a seamless flow within our e-commerce application. Here's how it might look:

// Creating a new product
var laptop = new Product("Laptop", 1200.00);

// Creating an address
var address = new Address("123 Main St", "Cityville", "12345");

// Creating a customer and placing an order
var customer = new Customer("John Doe", address);
customer.PlaceOrder(new List<Product> { laptop });

// Further interactions and methods...

By structuring our application using these DDD concepts, we achieve clarity, maintainability, and separation of concerns in our codebase. This comprehensive example demonstrates how entities, value objects, and aggregate roots contribute to a robust domain model, making our e-commerce application more adaptable to future changes and enhancements.

Entity, ValueObject, and AggregateRoot Classes

Here are the definitions for the Entity, ValueObject, and AggregateRoot classes that were used in the examples:

// Entity base class
public abstract class Entity
{
    public abstract object GetId();

    // Override Equals, GetHashCode, and equality operators
    // to compare entities by their identity
}

// ValueObject base class
public abstract class ValueObject
{
    // Override Equals and GetHashCode to compare value objects by their attributes
    // Implement equality operators if needed
}

// AggregateRoot base class
public abstract class AggregateRoot : Entity
{
    // Add methods for maintaining consistency and encapsulating internal objects
    // Typically, aggregate roots are responsible for enforcing business rules
    // and acting as the entry point for accessing the objects within the aggregate
}

Keep in mind that these base classes are provided as a foundation for implementing DDD concepts in your application. You can customize and extend them to fit the specific requirements of your domain and application.

Choosing Between Entity, Value Object, and Aggregate Root: When to Use Each

As we navigate the landscape of Domain-Driven Design (DDD), we find ourselves faced with the task of selecting the appropriate building blocks for our domain model. The triumvirate of Entity, Value Object, and Aggregate Root offers distinct advantages, but knowing when to use each requires a keen understanding of their characteristics and implications. Let's explore scenarios for employing these DDD constructs effectively.

When to Use an Entity

Entities shine when we deal with objects that possess a distinct identity and undergo changes while retaining that identity. Here are situations where entities are your go-to choice:

  1. Persistence and Identity: When objects need to be uniquely identifiable and persisted in a data store, use an entity. Examples include customers, products, or orders.

  2. Lifecycle with Changes: Entities are appropriate for objects that evolve over time while maintaining their identity. Consider customers with changing contact information or orders with updated status.

  3. Relationships and Associations: Entities are well-suited for objects involved in relationships or associations with other objects. Customers placing orders or authors of books are classic examples.

When to Use a Value Object

Value Objects come into play when the identity of the object isn't significant, and the emphasis is on the attributes defining it. Look for these situations to make value objects shine:

  1. Immutable Attributes: When an object's attributes are fixed and unchangeable after creation, use a value object. Examples include dates, addresses, and measurements.

  2. Equality and Comparison: If an object's equality is determined by the equality of its attributes, a value object is the right choice. This simplifies equality checks and comparisons.

  3. Aggregates and Components: Value objects are ideal for representing components within entities or aggregates. For instance, an address within a customer entity.

When to Use an Aggregate Root

Aggregate Roots provide structure and control over clusters of related objects. Deciding when to use them involves considering these scenarios:

  1. Transactional Consistency: When a group of objects must be updated together within a single transaction to maintain data integrity, use an aggregate root. For example, when placing an order involving customers, products, and addresses.

  2. Boundary Definition: If you need a clear boundary around a set of related objects to manage access and changes, an aggregate root is crucial. It encapsulates interactions and enforces business rules within its domain.

  3. Concurrency and Isolation: When managing concurrent access to related objects, using an aggregate root as a synchronization point helps maintain consistency and isolation.