No history yet

Domain Entity Modeling

Beyond Simple Data

You already know that entities represent core business concepts with a unique identity. But a model where entities are just bags of data properties is a missed opportunity. This leads to what's known as an Anemic Domain Model—a model where the business logic lives outside the objects themselves, usually in separate 'service' or 'manager' classes. The entities themselves are just data structures with getters and setters.

The problem? This separates the data from the rules that govern it. When logic is scattered, the system becomes harder to understand, maintain, and test. Business rules become duplicated or, worse, forgotten.

A Rich Domain Model, in contrast, combines data and behavior. The entities are not just passive data containers; they are active participants that enforce their own rules and invariants. An Order object doesn't just hold a list of items and a total price; it knows how to calculate that price, how to be confirmed, and what happens when an item is added. All the logic related to an order lives within the Order class.

Encapsulating Logic

One of the most powerful tools for creating rich models is the Value Object—a small, simple object that represents a descriptive aspect of the domain. Unlike entities, they have no conceptual identity. They are defined by their attributes. Two Money value objects are equal if they have the same currency and amount, regardless of which instance you're holding.

Value Objects help us avoid 'primitive obsession,' the habit of using basic types like string or decimal to represent complex concepts. For example, instead of using decimal for price, create a Money Value Object. This allows you to encapsulate rules directly related to money, like preventing the creation of a negative amount or handling currency conversions.

// A Value Object for Money
public class Money
{
    public decimal Amount { get; private set; }
    public string Currency { get; private set; }

    public Money(decimal amount, string currency)
    {
        if (amount < 0)
        {
            throw new ArgumentException("Amount cannot be negative.");
        }
        Amount = amount;
        Currency = currency;
    }

    // Method to add money, returning a new Money object (immutability)
    public Money Add(Money other)
    {
        if (this.Currency != other.Currency)
        {
            throw new InvalidOperationException("Cannot add different currencies.");
        }
        return new Money(this.Amount + other.Amount, this.Currency);
    }
}

Notice how the business rules—that money cannot be negative and that you can't add different currencies—are now embedded within the Money object itself. Any part of the system that uses Money automatically gets this protection for free. This is the essence of a rich model.

State and Invariants

Entities often have a lifecycle, moving through different states. An e-commerce order, for instance, might be Pending, Confirmed, Shipped, or Cancelled. The transitions between these states are governed by strict business rules, also known as domain invariants—conditions that must always be true for the entity to be valid.

A rich entity manages its own state transitions. Instead of having a public Status property that any outside code can change, the Order entity exposes methods like Confirm() or Ship(). These methods contain the logic to validate the transition.

public enum OrderStatus { Pending, Confirmed, Shipped, Cancelled }

public class Order
{
    public Guid Id { get; private set; }
    public OrderStatus Status { get; private set; }
    // ... other properties

    public Order()
    {
        Id = Guid.NewGuid();
        Status = OrderStatus.Pending;
    }

    public void Confirm()
    {
        if (Status != OrderStatus.Pending)
        {
            throw new InvalidOperationException("Only a pending order can be confirmed.");
        }
        Status = OrderStatus.Confirmed;
    }

    public void Ship()
    {
        if (Status != OrderStatus.Confirmed)
        {
            throw new InvalidOperationException("Only a confirmed order can be shipped.");
        }
        Status = OrderStatus.Shipped;
    }

    public void Cancel()
    {
        if (Status == OrderStatus.Shipped)
        {
            throw new InvalidOperationException("Cannot cancel an order that has already been shipped.");
        }
        Status = OrderStatus.Cancelled;
    }
}

This approach makes invalid state transitions impossible. The Order entity is the single authority on its own lifecycle. This model is pure—it has no knowledge of databases, user interfaces, or external frameworks. It is just a representation of the business process, making it incredibly easy to test and reason about.

This focus on purity is the goal. Your domain layer should be the stable, unchanging core of your application. Frameworks and databases will change over time, but your core business rules should remain consistent. By isolating them, you build a system that is resilient, adaptable, and a true reflection of the business it serves.

Quiz Questions 1/5

What is the primary characteristic of an Anemic Domain Model?

Quiz Questions 2/5

Which of the following is the best candidate for a Value Object?

This approach ensures that your core business logic is robust, testable, and independent of external concerns.