← Blog

Modeling the Model as Model Engineers

Marco Montorsi

Discover the concept of Digital Signature and its importance in cybersecurity.

In Domain-Driven Design (DDD), a Domain Model is a comprehensive, focused representation of the application domain. In a well-structured Domain Model, business logic is managed through specific components such as value objects, entities, aggregates. The ultimate goal is to reduce business logic complexity and ensure that code accurately reflects domain rules. In this article, we will explore the main elements of a Domain Model in DDD and see how they integrate to maintain consistency and simplify the handling of complex domain logic.


Building the Model: Value Objects, Entities, and Aggregates

The Domain Model consists of several core elements, each with a specific role, used to maintain the integrity of business logic.

1. Value Objects

Value Objects represent concepts identified solely by their values, without a unique identity. For example, a color can be defined by red, green, and blue values, as illustrated in the following example:

public class Color {
    public readonly byte Red;
    public readonly byte Green;
    public readonly byte Blue;

    public Color(byte red, byte green, byte blue) {
        this.Red = red;
        this.Green = green;
        this.Blue = blue;
    }
}

Value Objects are immutable: any change in values results in a new instance of the object. For instance, if we want to change the color, we can use a method like MixWith to create a new color by combining the RGB values of two existing colors:

public Color MixWith(Color other) {
    return new Color(
        (byte)Math.Min(this.Red + other.Red, 255),
        (byte)Math.Min(this.Green + other.Green, 255),
        (byte)Math.Min(this.Blue + other.Blue, 255)
    );
}

This immutability ensures that Value Objects can be used safely without side effects and are ideal for properties describing an entity’s characteristics.


2. Entities and Identity

Unlike Value Objects, Entities represent objects with a unique identity within the domain. They are identified by a specific ID that remains constant throughout the object’s lifecycle, while their state may change. For instance, a Person might be an entity with a unique ID that ensures identity:

public class Person {
    public readonly PersonId Id;
    public Name Name { get; set; }

    public Person(PersonId id, Name name) {
        this.Id = id;
        this.Name = name;
    }
}

In this example, PersonId is a Value Object that serves as the unique identifier for the person. This approach prevents ambiguity when there are people with similar names and ensures each person is treated as a unique instance in the system.


3. Aggregates: Managing Consistency with Boundaries

Aggregates are a fundamental concept in DDD to manage model consistency. An Aggregate is a set of entities and value objects that share a logical boundary and must be treated as a unit to ensure data consistency. The aggregate root is the main entity of the aggregate and is the only access point to modify its state from outside. In this way, the aggregate root maintains consistency and enforces business rules.

Consider an example of an aggregate based on a ticketing system, where a Ticket is the aggregate root that manages messages and other related entities:

public class Ticket {
    private List<Message> _messages;

    public void AddMessage(UserId from, string body) {
        var message = new Message(from, body);
        _messages.Add(message);
    }
}

In this case, the Ticket ensures that adding messages complies with domain rules. To maintain consistency, any operation that modifies the internal state, like AddMessage, must be executed through the aggregate root, which acts as the transactional boundary for all entities within.


Introduction to the Aggregate Pattern

When designing complex applications, especially in the context of Domain-Driven Design (DDD), the need often arises to ensure data consistency. A central pattern to achieve this is the Aggregate. An aggregate is not just an entity: it’s an entity with a specific identifying field whose state is subject to change throughout its lifecycle. The main goal of the aggregate pattern is to protect data consistency by defining clear boundaries within which changes can occur. In this article, we will explore how an aggregate works, its advantages, and mechanisms for maintaining its consistency.

Consistency and Aggregate Boundaries

Consistency is a crucial aspect of an aggregate. Since the state of an aggregate is modifiable, there’s a risk that data may become inconsistent. The aggregate pattern imposes a boundary structure that separates the aggregate from the rest of the application, acting as a "consistency boundary." This means that all changes to the aggregate’s state must comply with the associated business rules.

In implementation terms, consistency is ensured by allowing only the aggregate’s logic to modify its state. External objects can only read the aggregate’s state; all modifications must occur through its public interface methods, called commands.

Command Example

Commands are public methods that allow controlled changes. For example, suppose we have an aggregate called Ticket. To add a message to a ticket, we could expose a command called AddMessage:

public class Ticket
{
    public void AddMessage(UserId from, string body)
    {
        var message = new Message(from, body);
        _messages.Append(message);
    }
}

Alternatively, we can represent the command as an object that encapsulates all necessary data for execution:

public class Ticket
{
    public void Execute(AddMessage cmd)
    {
        var message = new Message(cmd.from, cmd.body);
        _messages.Append(message);
    }
}

In both cases, the business logic for adding a message is confined within the aggregate.

Managing Concurrency

Another critical aspect of an aggregate is concurrency management. In a multi-threaded or distributed context, multiple processes may attempt to update the same aggregate simultaneously, risking overwriting changes. To avoid conflicts, version control is used: each update increments the aggregate’s version. Before writing to the database, the system verifies that the current version matches the one read earlier.

For example, in SQL, version control could be implemented as follows:

UPDATE tickets
SET ticket_status = @new_status,
agg_version = agg_version + 1
WHERE ticket_id=@id AND agg_version=@expected_version;

In this way, if the read version doesn’t match, the operation fails, and the process can handle the error and retry. This approach is also applicable to NoSQL databases.

Transactional Boundaries

The aggregate also acts as a transactional boundary. Changes to the aggregate’s state must be executed as a single atomic operation: either all changes are saved, or none are. This principle prohibits transactions across multiple aggregates simultaneously, forcing careful design of aggregate boundaries to meet business requirements.

Entity Hierarchy and Value Objects

An aggregate can include other entities and value objects, creating an entity hierarchy. These objects are not autonomous and cannot exist separately from the main aggregate, as they are tied to its business rules. This allows for complex business scenarios where rules depend on the state of multiple objects.

For example, there may be a rule that requires the automatic reassignment of a ticket if an agent hasn’t responded within a certain timeframe:

public class Ticket
{
    List<Message> _messages;

    public void Execute(EvaluateAutomaticActions cmd)
    {
        if (this.IsEscalated && this.RemainingTimePercentage < 0.5 &&
            GetUnreadMessagesCount(for: AssignedAgent) > 0)
        {
            _agent = AssignNewAgent();
        }
    }

    public int GetUnreadMessagesCount(UserId id)
    {
        return _messages.Where(x => x.To == id && !x.WasRead).Count();
    }
}

In this example, the conditions for reassignment are verified atomically, ensuring the aggregate’s state remains consistent throughout the operation.

References between Aggregates

Each aggregate represents a separate transactional boundary. This means it’s not possible to include direct references to other aggregates, as this would violate the transactional boundary principle. Instead, dependencies between aggregates are implemented through identifiers. For instance, an Order aggregate may contain a reference to a Customer ID rather than a direct reference.

Conclusion

The aggregate pattern offers a robust approach to managing domain consistency and complexity. By defining clear boundaries and enforcing strict business rules, aggregates ensure that data remains consistent, even with concurrent updates or complex scenarios. However, the success of implementing this pattern depends on the ability to model transactional boundaries and hierarchical entity relationships to meet the domain’s business logic.

← All articles