Skip to main content
SEMastery
Architectureintermediate

Refactoring From an Anemic Domain Model to a Rich Domain Model in .NET

A friendly, step-by-step guide to turning a data-only anemic domain model into a rich domain model in C# .NET, with rules living inside your objects.

12 min readUpdated February 14, 2026

Imagine you order a fresh fruit juice from a small shop near your home. You tell the shopkeeper, "One sweet lime juice, please." You do not walk behind the counter, grab the blender, pour the sugar yourself, and switch on the machine. The shopkeeper does all of that. They know the recipe. They know how much sugar is safe. They keep the rules inside their own work.

Now imagine the opposite. The shop only gives you an empty glass and a pile of raw ingredients. You must mix everything yourself, every single time, in every shop you visit. If one customer adds too much sugar, nobody stops them. This second shop is messy, and mistakes happen often.

In software, these two shops are two ways of building your domain objects. The messy "do it yourself" shop is called an anemic domain model. The careful shopkeeper who guards the recipe is a rich domain model. This article shows you how to move from the first to the second, step by step, in C# and .NET.

What is an anemic domain model?

An anemic domain model is a class that holds only data. It has properties with public getters and setters, and almost no behavior. All the real rules live somewhere else, usually in big service classes.

Martin Fowler named this pattern back in 2003 and called it an anti-pattern. The word "anemic" means weak or lacking blood. These objects are weak because they carry no logic of their own. They are just bags of getters and setters.

Here is a typical anemic Order class.

public class Order
{
    public Guid Id { get; set; }
    public string Status { get; set; }
    public decimal Total { get; set; }
    public List<OrderItem> Items { get; set; } = new();
}
 
public class OrderItem
{
    public string ProductName { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}

Notice that anyone can write order.Status = "Anything" or order.Total = -500. Nothing stops them. The object cannot protect itself.

Now look at where the rules actually live. They sit far away, inside a service.

public class OrderService
{
    public void AddItem(Order order, OrderItem item)
    {
        if (order.Status == "Shipped")
            throw new InvalidOperationException("Cannot change a shipped order.");
 
        order.Items.Add(item);
        order.Total = order.Items.Sum(i => i.UnitPrice * i.Quantity);
    }
}

This works on day one. The trouble starts when a second service also adds items, and a third service changes the total a different way, and a fourth forgets the shipped check. The rules drift apart. Bugs creep in.

In an anemic model, the rules live outside the data, scattered across services.

What is a rich domain model?

A rich domain model puts the rules back inside the object that owns the data. The Order class itself decides what is allowed. Other code must ask the order to do things, instead of reaching in and changing fields.

This matches the advice from Microsoft Learn: an entity should not have public setters. Changes happen through clear methods that describe the business action, like AddItem or MarkAsShipped.

In a rich model, the rules live inside the entity. Callers ask the entity to act.

The big difference is encapsulation. Encapsulation means hiding the inside of an object and only allowing safe, named actions from the outside. The juice shopkeeper encapsulates the recipe. Your Order should encapsulate its rules.

A quick comparison

Before we refactor, let us line up the two styles side by side.

TopicAnemic modelRich model
Where rules liveIn service classesInside the entity
SettersPublic, anyone can changePrivate, controlled by methods
Object validityCan be invalidAlways valid after each method
Duplication riskHigh, rules repeat in servicesLow, one trusted place
Reading the codeLogic is spread outLogic reads like the business

And here is when each one fits best.

SituationBest choiceWhy
Simple CRUD, almost no rulesAnemicLess code, easy to follow
Many changing business rulesRichOne safe home for logic
Short-lived prototypeAnemicSpeed matters more
Long-lived core systemRichSafety and clarity matter more

So an anemic model is not always wrong. For a tiny CRUD service it is fine. The pain appears when rules grow and scatter.

The refactoring journey

Let us walk through the steps to turn our weak Order into a strong one. We will go slowly so each move makes sense.

Refactoring steps

Close setters
Add factory
Add behavior methods
Protect collections
Move rules in

Steps

1

Close setters

Make setters private

2

Add factory

Create valid objects only

3

Add behavior methods

Name the business actions

4

Protect collections

Hide the list

5

Move rules in

Empty the service

The path from anemic to rich, one safe move at a time.

Step 1: Close the public setters

The first move is to stop outsiders from changing fields directly. We make every setter private. Now the only code that can change an order is the order itself.

public class Order
{
    public Guid Id { get; private set; }
    public OrderStatus Status { get; private set; }
    public decimal Total { get; private set; }
 
    private readonly List<OrderItem> _items = new();
}

Right away the object is safer. Nobody can write order.Total = -500 from outside anymore. We also swapped the loose string Status for an enum OrderStatus, so only real statuses are possible.

Step 2: Create objects through a controlled path

If the constructor is open and empty, people can still build a broken order. So we add a constructor (or a small factory method) that checks the basics and refuses to create something invalid.

public class Order
{
    public Guid Id { get; private set; }
    public OrderStatus Status { get; private set; }
 
    private Order() { } // for EF Core
 
    public Order(Guid customerId)
    {
        if (customerId == Guid.Empty)
            throw new ArgumentException("An order needs a real customer.");
 
        Id = Guid.NewGuid();
        Status = OrderStatus.Draft;
    }
}

A newborn order is now always valid. It starts as a Draft with a real customer. The private parameterless constructor is just for Entity Framework Core, which can use private members to rebuild objects from the database.

Step 3: Add behavior methods that speak the business language

This is the heart of the change. Each real action becomes a method with a clear name. The method holds the rule. Callers must go through it.

public void AddItem(string productName, int quantity, decimal unitPrice)
{
    if (Status != OrderStatus.Draft)
        throw new InvalidOperationException("Only draft orders can be changed.");
 
    if (quantity <= 0)
        throw new ArgumentException("Quantity must be at least 1.");
 
    _items.Add(new OrderItem(productName, quantity, unitPrice));
    RecalculateTotal();
}
 
public void MarkAsShipped()
{
    if (!_items.Any())
        throw new InvalidOperationException("Cannot ship an empty order.");
 
    Status = OrderStatus.Shipped;
}
 
private void RecalculateTotal() =>
    Total = _items.Sum(i => i.UnitPrice * i.Quantity);

Read those method names out loud: add item, mark as shipped. They sound like the business. This is the ubiquitous language idea from Domain-Driven Design. The code now tells the same story the shopkeeper would tell.

Step 4: Protect the collection

Our _items list is private, which is good. But callers still need to see the items, for example to show them on a screen. The trick is to expose a read-only view, not the real list. If we handed out the real list, anyone could call Items.Add(...) and skip our rules.

private readonly List<OrderItem> _items = new();
 
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();

Now outside code can read the items but cannot add or remove them. The only way to add an item is through AddItem, which checks the rules first. Microsoft Learn recommends exactly this: child collections should be read-only from the outside.

The aggregate root guards everything inside its boundary.

Step 5: Empty out the service

With all the rules inside Order, the old OrderService becomes very thin. It now only loads the order, asks it to do something, and saves it. This thin layer is sometimes called an application service.

public class OrderAppService
{
    private readonly IOrderRepository _orders;
 
    public OrderAppService(IOrderRepository orders) => _orders = orders;
 
    public async Task AddItemAsync(Guid orderId, string product, int qty, decimal price)
    {
        var order = await _orders.GetAsync(orderId);
        order.AddItem(product, qty, price); // the rule lives here, not here-ish
        await _orders.SaveAsync(order);
    }
}

The service no longer knows the rules. It just coordinates. If the rule for adding an item ever changes, you change it in one place: the Order class.

The aggregate root idea

Our Order is now what Domain-Driven Design calls an aggregate root. An aggregate is a small group of related objects that change together. In our case, the Order and its OrderItem children form one aggregate. The Order is the root, the single front door.

The rule is simple: nobody talks to an OrderItem directly from outside. They go through the Order. This keeps the whole group consistent, because the root checks every change.

Aggregate boundary

Caller
Order (root)
OrderItem
OrderItem

Steps

1

Caller

Calls root methods only

2

Order (root)

Checks rules, updates children

3

OrderItem

Changed only via root

4

OrderItem

Never touched directly

Outside code only touches the root. Children stay protected.

Think of the aggregate as a tiffin box. The lid is the root. You open the lid to put food in or take it out. You do not poke holes in the side of the box. One opening, one set of rules.

How Entity Framework Core fits in

A common worry is, "If my setters are private and my constructor is hidden, how does the database load my object?" The good news is that EF Core in .NET 10 handles this well. It can use private setters, backing fields like _items, and a private parameterless constructor.

You map the backing field in your configuration so EF Core knows how to fill the list.

public class OrderConfig : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.HasKey(o => o.Id);
 
        var nav = builder.Metadata.FindNavigation(nameof(Order.Items));
        nav!.SetPropertyAccessMode(PropertyAccessMode.Field);
    }
}

This tells EF Core to read and write the _items field directly, instead of going through the read-only Items property. Your encapsulation stays intact, and persistence still works. You do not need any extra library or framework for this. Plain EF Core and the standard .NET SDK are enough.

A few honest warnings

Moving to a rich model is a good idea for systems with real rules, but please keep these in mind.

  • Do not over-engineer a simple CRUD app. If your "order" is just a row with no rules, a rich model adds work for little gain.
  • Keep aggregates small. A giant aggregate that owns half the database becomes slow and hard to load.
  • Refactor in small steps. Close setters first, then add one method at a time, and keep your tests green between each move.
  • Avoid leaking the internal list. The most common slip is returning _items directly instead of a read-only view.

You may have heard of message libraries like MediatR and MassTransit, which often appear in DDD examples for sending domain events. As of 2026 both have moved to a commercial license for many uses. You do not need either of them to build a rich domain model. They are optional extras, so check the license terms before adding them to a project.

Putting it all together

Here is the shape of the finished Order. The data is private. The rules are inside. The object is always valid.

MemberVisibilityPurpose
Id, Status, TotalRead-only from outsideShow state, never set directly
ItemsRead-only collectionView items, cannot edit list
AddItemPublic methodAdd an item with rules checked
MarkAsShippedPublic methodChange status safely
RecalculateTotalPrivate methodInternal helper, hidden

The class now reads like the business. A new teammate can open it and learn the rules of an order just by reading the method names. That is the real prize of a rich domain model: the code explains itself.

Quick recap

  • An anemic domain model is a class with only data and no behavior. Rules live far away in services.
  • A rich domain model puts the rules inside the object, protected by encapsulation.
  • Refactor in small steps: make setters private, add a checking constructor, add named behavior methods, protect the collection, then empty the service.
  • The Order becomes an aggregate root: the single front door for itself and its child items.
  • Entity Framework Core supports private setters, backing fields, and private constructors, so your encapsulation and your database both stay happy.
  • An anemic model is not always wrong. For simple CRUD it is fine. Go rich when business rules grow and start to scatter.
  • You do not need MediatR or MassTransit (now commercially licensed) to build a rich model. Plain C# and the .NET SDK are enough.

References and further reading

Related Posts