# Rich Domain Model vs Anemic Domain Model in C#

> An anemic domain model separates data from behavior. A rich domain model keeps them together. Here is why it matters, how to refactor from anemic to rich, and when each approach is appropriate.

Published: 2026-09-22. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/rich-vs-anemic-domain-model

A **rich domain model** keeps business rules with the entities and value objects that own the state, while an **anemic model** stores data in objects and implements behavior in services.
Use a rich model for meaningful invariants and state transitions.
For straightforward CRUD, a simpler data model and transaction scripts can be sufficient.

## Two Approaches to Domain Modeling

Martin Fowler described the [**Anemic Domain Model**](https://martinfowler.com/bliki/AnemicDomainModel.html) as an anti-pattern in 2003: it pays the cost of a domain object model while leaving its behavior in services.

Let's look at both approaches and understand when each one is actually the right choice.

| Concern | Anemic Model | Rich Model |
| --- | --- | --- |
| Business rules | Implemented in services or transaction scripts | Implemented in entities, value objects, and domain services |
| State changes | Callers assign properties directly | Callers invoke operations that validate transitions |
| Collections | Often exposed as mutable lists | Private storage with read-only views and controlled mutations |
| Testing | Test the service that owns each use case | Test local rules directly through the domain API |
| Best fit | Simple CRUD and data-centric workflows | Domains with substantial rules and state transitions |
| Cost | Callers must consistently apply validation | More modeling and persistence configuration |

![Comparison showing the anemic model with all rules in OrderService mutating a data-bag Order, versus the rich model where the application service orchestrates and the Order aggregate holds rules and invariants](https://milanjovanovic.tech/blogs/articles/rich-vs-anemic-domain-model/anemic-vs-rich.png)

## The Anemic Domain Model

An anemic model separates data from behavior.
Entities are data containers - just properties with getters and setters.
All logic lives in services.

```csharp
// Anemic entity - just data
public class Order
{
    public Guid Id { get; set; }
    public Guid CustomerId { get; set; }
    public List<OrderLineItem> LineItems { get; set; } = new();
    public string Status { get; set; } = "Draft";
    public decimal TotalAmount { get; set; }
    public DateTime CreatedAt { get; set; }
}

// All logic in a service
public class OrderService
{
    public void AddLineItem(Order order, Guid productId, decimal price, int quantity)
    {
        if (order.Status != "Draft")
            throw new InvalidOperationException("Cannot modify non-draft order.");

        if (quantity <= 0)
            throw new ArgumentException("Quantity must be positive.");

        order.LineItems.Add(new OrderLineItem
        {
            ProductId = productId,
            Price = price,
            Quantity = quantity
        });

        order.TotalAmount = order.LineItems.Sum(li => li.Price * li.Quantity);
    }

    public void Cancel(Order order)
    {
        if (order.Status == "Cancelled")
            throw new InvalidOperationException("Order already cancelled.");

        if (order.Status == "Shipped")
            throw new InvalidOperationException("Cannot cancel shipped order.");

        order.Status = "Cancelled";
    }
}
```

Problems:
- **Anyone can modify `order.Status` directly** - bypassing all validation
- **Business rules are scattered** - which service validates what?
- **Entities don't protect themselves** - invalid states are easy to create
- **Testing requires the service** - you can't test order behavior in isolation

## The Rich Domain Model

A rich model keeps data and behavior together.
Entities enforce their own invariants.

The `Order.cs` excerpt uses application-defined IDs, immutable `Money`, domain events, and the `Result` types from the linked Result-pattern article.
Assume `CustomerId` and `ProductId` validate their underlying IDs at creation; a line item exposes no public mutation methods:

```csharp
public sealed class Order : AggregateRoot
{
    private readonly List<OrderLineItem> _lineItems = new();

    private Order(Guid id, CustomerId customerId) : base(id)
    {
        CustomerId = customerId;
        Status = OrderStatus.Draft;
        CreatedAt = DateTime.UtcNow;
    }

    public CustomerId CustomerId { get; }
    public OrderStatus Status { get; private set; }
    public Money TotalAmount { get; private set; } = Money.Zero(Currency.Usd);
    public DateTime CreatedAt { get; }
    public IReadOnlyCollection<OrderLineItem> LineItems => _lineItems.AsReadOnly();

    public static Order Create(CustomerId customerId)
    {
        ArgumentNullException.ThrowIfNull(customerId);

        var order = new Order(Guid.NewGuid(), customerId);
        order.RaiseDomainEvent(new OrderCreatedDomainEvent(order.Id));
        return order;
    }

    public Result AddLineItem(ProductId productId, Money price, int quantity)
    {
        if (Status != OrderStatus.Draft)
        {
            return Result.Failure(OrderErrors.NotDraft);
        }

        if (quantity <= 0)
        {
            return Result.Failure(OrderErrors.InvalidQuantity);
        }

        ArgumentNullException.ThrowIfNull(productId);
        ArgumentNullException.ThrowIfNull(price);

        if (price.Currency != Currency.Usd)
        {
            return Result.Failure(new Error(
                "Order.CurrencyMismatch", "Order prices must be in USD."));
        }

        var lineItem = new OrderLineItem(productId, price, quantity);
        var newTotal = TotalAmount + lineItem.TotalPrice;
        _lineItems.Add(lineItem);
        TotalAmount = newTotal;

        return Result.Success();
    }

    public Result Cancel()
    {
        if (Status == OrderStatus.Cancelled)
        {
            return Result.Failure(OrderErrors.AlreadyCancelled);
        }

        if (Status == OrderStatus.Shipped)
        {
            return Result.Failure(OrderErrors.CannotCancelShipped);
        }

        Status = OrderStatus.Cancelled;
        RaiseDomainEvent(new OrderCancelledDomainEvent(Id));

        return Result.Success();
    }

}
```

Benefits:
- **Private setters** - state can only change through controlled methods
- **Private constructor** - creation goes through `Create()` which enforces invariants
- **Read-only collections** - external code can't modify `_lineItems` directly
- **Business rules encapsulated** - the entity validates itself
- [**Domain events**](https://milanjovanovic.tech/blog/domain-events-vs-integration-events) - the entity signals when something notable happens
- [**Result pattern**](https://milanjovanovic.tech/blog/functional-error-handling-in-dotnet-with-the-result-pattern) - expected failures appear in the method contract

The new total is computed before mutating the collection, so a failed calculation does not leave a partially updated order.
This example fixes the order currency at USD; the `Money` type must enforce non-negative prices and currency-safe arithmetic.

## Refactoring From Anemic to Rich

Here's the step-by-step approach for [**refactoring to a rich domain model**](https://milanjovanovic.tech/blog/refactoring-from-an-anemic-domain-model-to-a-rich-domain-model):

### Step 1: Make Setters Private

```csharp
// Before
public string Status { get; set; }

// After
public OrderStatus Status { get; private set; }
```

This immediately breaks all code that directly modifies `Status` - which is exactly what you want.
It forces you to create proper methods.

### Step 2: Add Domain Methods

Move logic from services into the entity:

```csharp
// Before - logic in service
orderService.Cancel(order);

// After - logic on entity
order.Cancel();
```

### Step 3: Protect Collections

```csharp
// Before - anyone can Add/Remove
public List<OrderLineItem> LineItems { get; set; }

// After - controlled through methods
private readonly List<OrderLineItem> _lineItems = new();
public IReadOnlyCollection<OrderLineItem> LineItems => _lineItems.AsReadOnly();
```

### Step 4: Replace Primitives With Value Objects

```csharp
// Before
public decimal TotalAmount { get; set; }
public string Currency { get; set; }

// After
public Money TotalAmount { get; private set; }
```

[**Value objects**](https://milanjovanovic.tech/blog/value-objects-in-dotnet-ddd-fundamentals) centralize local validation when their constructors, factories, and public APIs enforce the same rules.
They do not replace null handling, database constraints, or concurrency checks.

### Step 5: Add Factory Methods

```csharp
// Before - public constructor, any state possible
var order = new Order { Status = "Invalid", TotalAmount = -100 };

// After - controlled creation
var order = Order.Create(customerId);
```

## When Is a Simpler Data Model Appropriate?

The anemic model isn't always wrong.
It's the right choice when:

**Simple CRUD operations.** If your "business logic" is just saving data to a database and reading it back, a rich model adds complexity without value.

**Data-centric applications.** Reporting dashboards, admin panels, data import tools - these work with data, not domain behavior.

**Small, simple domains.** If your entities have no invariants to protect, there's nothing for a rich model to encapsulate.

## When to Use a Rich Model

**Complex business rules.** If entities have invariants, state transitions, and validation rules, put them on the entity.

**Multiple consumers of the same logic.** If three services all check `if (order.Status == "Draft")` before modifying an order, that check belongs on the entity - once.

**Long-lived projects.** The investment in a rich model pays off over time as the codebase grows and business rules become more complex.

**Domain-Driven Design.** A rich model is useful for a complex core domain, but [**DDD**](https://milanjovanovic.tech/blog/domain-driven-design-dotnet-getting-started) also includes strategic modeling and boundaries.
A supporting CRUD area can use a simpler implementation without invalidating those architectural decisions.

## The Application Service's Role Changes

With a rich model, the [**Application layer**](https://milanjovanovic.tech/blog/application-layer-clean-architecture) becomes thinner:

```csharp
// With anemic model - service does everything
public async Task<Result> CancelOrder(Guid orderId)
{
    var order = await _repository.GetByIdAsync(orderId);

    if (order is null)
        return Result.Failure(new Error("Order.NotFound", "Order not found."));

    if (order.Status == "Cancelled")
        return Result.Failure(new Error("Order.AlreadyCancelled", "Already cancelled."));

    if (order.Status == "Shipped")
        return Result.Failure(new Error("Order.Shipped", "Cannot cancel shipped order."));

    order.Status = "Cancelled";

    await _unitOfWork.SaveChangesAsync();

    await _eventBus.PublishAsync(new OrderCancelledEvent(orderId));

    return Result.Success();
}

// With rich model - service orchestrates
public async Task<Result> CancelOrder(Guid orderId)
{
    var order = await _repository.GetByIdAsync(orderId);

    if (order is null)
        return Result.Failure(new Error("Order.NotFound", "Order not found."));

    var result = order.Cancel(); // Domain logic on entity

    if (result.IsFailure)
        return result;

    await _unitOfWork.SaveChangesAsync(); // Persistence infrastructure captures events

    return Result.Success();
}
```

The Application Service goes from implementing business logic to **orchestrating** domain objects.
`SaveChangesAsync` does not dispatch domain events by itself.
Configure a dispatcher explicitly, or persist outgoing events in an [**outbox**](https://milanjovanovic.tech/blog/implementing-the-outbox-pattern) with the order and deliver them afterward.
Use an [**EF Core concurrency token**](https://learn.microsoft.com/en-us/ef/core/saving/concurrency) that changes for every aggregate modification when simultaneous requests can violate its rules.

## Summary

A simple data model is sufficient for straightforward CRUD.
A rich model gives complex business rules a controlled API and reduces the number of callers that must know each invariant.

If you're doing DDD or building a complex .NET application, invest in rich entities that:
- Protect their invariants
- Expose behavior through methods
- Use private setters and read-only collections
- Raise domain events for side effects

Start with the most important [**aggregate**](https://milanjovanovic.tech/blog/aggregate-design-ddd) and work outward.

Thanks for reading.

And stay awesome!

---

## Frequently asked questions

### What is an anemic domain model?

An anemic domain model separates data from behavior: entities are property bags with public getters and setters, and all business logic lives in services. Martin Fowler described it as an anti-pattern because it gives up the encapsulation benefits of objects.

### What is a rich domain model?

A rich domain model keeps data and behavior together. Entities have private setters, expose operations as methods, validate their own invariants, and raise domain events when something notable happens.

### Is the anemic domain model always bad?

No. For simple CRUD applications, data-centric tools, and domains with no real invariants, an anemic model is simpler and perfectly adequate. The rich model pays off when business rules are complex.

### How do you refactor from an anemic to a rich domain model?

Make setters private, move logic from services into entity methods, replace exposed collections with read-only views, replace primitives with value objects, and add factory methods for controlled creation.

### Where does business logic go with a rich domain model?

Rules live in entities, value objects, and domain services for policies that do not fit one object. Application services orchestrate loading, domain operations, persistence, and returning results.
