What are Domain Models

Domain modeling is a crucial part of Domain Driven Design, as it provides the blueprint for representing and implementing the core business...

Written by Bala · 5 min read >

Domain modeling is a crucial part of Domain Driven Design, as it provides the blueprint for representing and implementing the core business logic in a software system. This blog will delve into the essentials of domain modeling, exploring key concepts such as entities, value types, aggregates, and factories, and illustrating their use with practical examples and C# 12 code snippets.

What Constitutes a Domain Model?

A domain model is a conceptual representation of the business domain, capturing its essential aspects such as entities, value types, and the relationships between them. The primary purpose of a domain model is to create a shared understanding of the domain among stakeholders, facilitating better communication and alignment. While an object-oriented approach may be more suitable for the domain model, it can also be effectively modeled using functional programming techniques. Whether you choose functional programming or object-oriented programming, both are just tools or options available to represent the domain model.

The domain model typically includes Entities, Value Types, Aggregates or Aggregate Roots and Factories. We shall see what these are in detail.

Entities

Entities are objects with a distinct identity that can change state over time. They are characterized by a unique identifier and lifecycle, and they encapsulate business rules and behaviors relevant to their domain. For example, a Customer class can be an entity with a unique CustomerId. It also includes properties like Name, Email, and Address, and behaviors such as PlaceOrder and UpdateAddress.

public class Customer
{
    public Guid CustomerId { get; private set; }
    public string Name { get; private set; }
    public string Email { get; private set; }
    public string Gender{ get; private set; }

    public Customer(Guid customerId, string name, string email, string gender)
    {
        CustomerId = customerId;
        Name = name;
        Email = email;
        Address = gender;
    }

    public void PlaceOrder(Order order)
    {
        // Business logic for placing an order
    }

    public void UpdateAddress(string newAddress)
    {
        Address = newAddress;
    }
}

Value Types

A value type, or value object, is defined by its attributes rather than a unique identity. Value types are immutable and are used to describe specific attributes of entities. Consider the 'Money' value type in a accounting application.

  • It could contain attributes. For example in the case of the accounting application the Money could contain Amount and Currency as it two main properties.
  • It is immutable, meaning that once its created it cannot be changed.
  • It too like the entities does contain behavior or methods which operate on those properties.
public class Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public Money Add(Money other)
    {
        if (Currency != other.Currency)
        {
            throw new InvalidOperationException("Cannot add money with different currencies.");
        }
        return new Money(Amount + other.Amount, Currency);
    }

    public Money Subtract(Money other)
    {
        if (Currency != other.Currency)
        {
            throw new InvalidOperationException("Cannot subtract money with different currencies.");
        }
        return new Money(Amount - other.Amount, Currency);
    }
}

Here is another example of an Entity and a Value Type. Consider an Order entity and a DateRange value type. Order entity has a unique OrderId, it contains order items and includes behavior such as AddItem and Remove Item. DateRange on the other hand represents a period with a start and end date, and include behaviors such as checking if a date falls within a range.

public class Order
{
    public Guid OrderId { get; private set; }
    public List<OrderItem> Items { get; private set; } = new List<OrderItem>();

    public Order(Guid orderId)
    {
        OrderId = orderId;
    }

    public void AddItem(OrderItem item)
    {
        Items.Add(item);
    }

    public void RemoveItem(OrderItem item)
    {
        Items.Remove(item);
    }
}

public class DateRange
{
    public DateTime StartDate { get; }
    public DateTime EndDate { get; }

    public DateRange(DateTime startDate, DateTime endDate)
    {
        if (startDate > endDate)
        {
            throw new ArgumentException("Start date must be earlier than end date.");
        }
        StartDate = startDate;
        EndDate = endDate;
    }

    public bool IsDateWithinRange(DateTime date)
    {
        return date >= StartDate && date <= EndDate;
    }
}

Aggregates

An aggregate is a cluster of related entities and value types treated as a single unit for data changes. The Aggregate Root is the main entity that provides access to the aggregate and ensures consistency. In this scenario, the Order aggregate acts as the root entity that manages a collection of OrderItems. The Order ensures that all business rules are enforced, such as validating the total amount, checking inventory, and applying discounts. Each OrderItem represents a specific item within the order and relies on the Order aggregate for its existence and context. This means that an OrderItem cannot exist independently outside of an Order, ensuring that all operations and validations are centralized within the Order aggregate. This design helps maintain consistency and integrity within the system.

public class Order
{
    public Guid OrderId { get; private set; }
    private List<OrderItem> _items = new List<OrderItem>();

    public Order(Guid orderId)
    {
        OrderId = orderId;
    }

    public void AddItem(OrderItem item)
    {
        _items.Add(item);
    }

    public void RemoveItem(OrderItem item)
    {
        _items.Remove(item);
    }

    public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
}

public class OrderItem
{
    public Guid ItemId { get; private set; }
    public string ProductName { get; private set; }
    public int Quantity { get; private set; }

    public OrderItem(Guid itemId, string productName, int quantity)
    {
        ItemId = itemId;
        ProductName = productName;
        Quantity = quantity;
    }
}

Identifying entities, value types, and aggregates, along with their roots, is a skill that develops with experience and practice. I often get it wrong initially, but through reflection and review, I manage to get it right. However, as you know, there is no single silver bullet in software design; everything involves tradeoffs. I'll probably write a separate blog on this topic when time permits.

Factories or Factory Methods

In Domain-Driven Design (DDD), Factory Methods or Factories are used to encapsulate the creation logic of entities or value objects, ensuring that the invariants of the domain model are maintained during the creation process. Factories can be implemented in various ways in C#, and with the new features in C# 12, there are some enhancements that can simplify the implementation. Here, I'll provide examples for several different factory patterns that are generally used in Domain Driven Design.

Simple Factory Method

A Factory Method is a static method in the entity class that is responsible for creating an instance of that entity. This method ensures that the entity is created in a consistent state.

public class Order
{
    public Guid Id { get; private set; }
    public string CustomerName { get; private set; }
    public DateTime OrderDate { get; private set; }

    private Order(Guid id, string customerName, DateTime orderDate)
    {
        Id = id;
        CustomerName = customerName;
        OrderDate = orderDate;
    }

    public static Order Create(Guid id, string customerName, DateTime orderDate)
    {
        if (string.IsNullOrWhiteSpace(customerName))
        {
            throw new ArgumentException("Customer name cannot be empty");
        }

        if (orderDate > DateTime.UtcNow)
        {
            throw new ArgumentException("Order date cannot be in the future");
        }

        return new Order(id, customerName, orderDate);
    }
}

For most use cases, the above implementation would suffice. However, there are instances where large aggregate root classes necessitate a separate creation process for the sake of readability and maintainability. In the subsequent sections, I have enumerated on some of the other options available for entity creation.

Factory Class

A Factory Class is a separate class that contains methods for creating entities. This approach is useful when the creation logic is complex or when you want to centralize creation logic.

public class OrderFactory
{
    public Order CreateOrder(Guid id, string customerName, DateTime orderDate)
    {
        if (string.IsNullOrWhiteSpace(customerName))
        {
            throw new ArgumentException("Customer name cannot be empty");
        }

        if (orderDate > DateTime.UtcNow)
        {
            throw new ArgumentException("Order date cannot be in the future");
        }

        return new Order(id, customerName, orderDate);
    }
}

Builder Pattern

The Builder Pattern is useful when an object needs to be created in multiple steps in an expressive way.

public class OrderBuilder
{
    private Guid _id;
    private string _customerName;
    private DateTime _orderDate;

    public OrderBuilder WithId(Guid id)
    {
        _id = id;
        return this;
    }

    public OrderBuilder WithCustomerName(string customerName)
    {
        _customerName = customerName;
        return this;
    }

    public OrderBuilder WithOrderDate(DateTime orderDate)
    {
        _orderDate = orderDate;
        return this;
    }

    public Order Build()
    {
        if (string.IsNullOrWhiteSpace(_customerName))
        {
            throw new ArgumentException("Customer name cannot be empty");
        }

        if (_orderDate > DateTime.UtcNow)
        {
            throw new ArgumentException("Order date cannot be in the future");
        }

        return new Order(_id, _customerName, _orderDate);
    }
}

Abstract Factory Pattern

The Abstract Factory Pattern is used when you need to create families of related or dependent objects without specifying their concrete classes

public interface IOrderFactory
{
    Order CreateOrder(Guid id, string customerName, DateTime orderDate);
}

public class RegularOrderFactory : IOrderFactory
{
    public Order CreateOrder(Guid id, string customerName, DateTime orderDate)
    {
        if (string.IsNullOrWhiteSpace(customerName))
        {
            throw new ArgumentException("Customer name cannot be empty");
        }

        if (orderDate > DateTime.UtcNow)
        {
            throw new ArgumentException("Order date cannot be in the future");
        }

        return new Order(id, customerName, orderDate);
    }
}

Relationship Between Factory Methods and Aggregate Roots

Quite interestingly, the Factory Method is actually found in the class of the Aggregate Root. In the previous example, the class containing the method Create is in the Order class. The link between Factory Methods and Aggregate Roots is established through the need for controlled creation and initialization of Aggregates.

Factory methods are crucial in Domain-Driven Design because they encapsulate the logic of creating an Aggregate. This ensures that aggregates are always created in a consistent and valid state, maintaining their integrity and invariants. By enforcing rules and constraints during creation, factory methods prevent invalid and inconsistent states right from the start. This approach helps build systems that are robust and reliable.

In my next set of posts, I will delve into more advance concepts related to the domain model such as how to validate domain models and on the how to avoid the use of primitive obsession with the use of Value Types.

Leave a Reply

Your email address will not be published. Required fields are marked *