HomeNikola Knezevic

In this article

Banner

Anemic vs Rich Models in ASP.NET Core

12 Mar 2026
5 min

Special Thanks to Our Sponsors:

Hypereal AI Logo

Hypereal AI gives you one API and 50+ models. It's a serverless platform.

Flux, Sora, Kling, Nano Banana. Switch models with one parameter.

If you want to experiment, grab the free tokens and test it yourself:

👉 Learn more

Sponsor Newsletter

When working with domain heavy application, one of the biggest concerns is directed at the domain.

Domain models can just store data, however, they can also contain core business behavior of each model.

Depending on your approach designing models can end up being anemic or rich domain models.

Understanding the difference between them and knowing when to move from one to the other is key to building maintainable and expressive systems.

Anemic Models

An Anemic Domain Model is a model where entities act as data containers only.

They have properties but no behavior while all business rules are placed in separate service classes:

csharp
public class Order
{
    public Guid Id { get; set; }
    public List<OrderItem> Items { get; set; } = [];
    public decimal TotalPrice { get; set; }
    public string Status { get; set; } = "Pending";
}

And it's completely fine for simple CRUD applications, customer-facing APIs etc.

It's simple to work with models, there are no constraints, feels easier at first glance.

However, as your application grows, especially when it's domain heavy you start noticing how scattered business logic and rules are harder to maintain, domain integrity can easily be broken, with weak encapsulation.

Rich Models

That's where Rich models jump in.

A Rich Domain Model encapsulates both data and behavior.

It aims to represent business concepts as real-world objects that enforce their own rules. It also follows principles of DDD.

Instead of being passive data carriers, entities become active participants in your services:

csharp
public class Order
{
    private readonly List<OrderItem> _items = [];
    public Guid Id { get; private set; }
    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
    public decimal TotalPrice => _items.Sum(i => i.Price);
    public string Status { get; private set; } = "Pending";

    public void AddItem(OrderItem item)
    {
        if (Status == "Completed")
            throw new InvalidOperationException("Cannot add items to a completed order.");

        _items.Add(item);
    }

    public void Complete()
    {
        if (!_items.Any())
            throw new InvalidOperationException("Cannot complete an empty order.");

        Status = "Completed";
    }
}

Now, Order controls its own state and ensures consistency. Business rules are closer to the data, making the model more expressive and resilient to misuse.

This solves the issues anemic models bring, strong encapsulation, domain integrity and it's easier to reason and maintain business rules.

The caveat with rich domain models is that they require a bit deeper understanding of the domain, a bit steeper learning curve since it usually comes packed with a lot of other domain driven principles that make understanding models even harder for people unfamiliar with DDD.

Anemic to Rich Models

Most applications don't start with rich models and why should they.

It’s perfectly fine to begin with an anemic model when your domain is simple or not well understood at the beginning.

As you need to fulfill more use cases, your domain model can incorporate more rules.

If you leave models anemic for some time as complexity grows, you'll notice:

  • Business rules start repeating across services.
  • Entity invariants become harder to maintain.
  • Bugs arise due to inconsistent rule enforcement.

And that is the right moment to refactor toward a rich domain model.

Here's an example of a use case using the anemic model from above:

csharp
internal sealed class CancelOrderCommandHandler(IApplicationDbContext dbContext)
    : IRequestHandler<CancelOrderCommand, Unit>
{
    public async Task<Unit> Handle(CancelOrderCommand request, CancellationToken cancellationToken)
    {
        var order = await dbContext.Orders
            .FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);

        if (order == null)
        {
            throw new Exception($"Order with identifier {request.OrderId} not found.");
        }

        if (order.Status is OrderStatus.Delivered)
            throw new InvalidOperationException("Delivered orders cannot be cancelled.");

        if (order.Status is OrderStatus.Cancelled)
            throw new InvalidOperationException("Cancelled orders cannot be cancelled.");

        order.Status = OrderStatus.Cancelled;

        await dbContext.SaveChangesAsync(cancellationToken);
        return Unit.Value;
    }
}

Because all business rules are placed in a use case, now every time we need something we need to copy-paste all business rules. If one business rule is changed everywhere it must be changed.

By simply moving business rules inside a method of a model we can easily encapsulate these rules and have them be reusable whenever we need a certain behavior:

csharp
internal sealed class CancelOrderCommandHandler(IApplicationDbContext dbContext)
    : IRequestHandler<CancelOrderCommand, Unit>
{
    public async Task<Unit> Handle(CancelOrderCommand request, CancellationToken cancellationToken)
    {
        var order = await dbContext.Orders
            .FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);

        if (order == null)
        {
            throw new Exception($"Order with identifier {request.OrderId} not found.");
        }

        order.Cancel();
        await dbContext.SaveChangesAsync(cancellationToken);
        return Unit.Value;
    }
}

Any change request results regarding business rules affects only the model making it easier to maintain.

Here are some refactoring tips that really helped me:

  • Move logic in smaller chunks from services to entities themselves.
  • Replace property setters with private set; and expose methods for update.
  • Always try to separate core business rules from the business layer to domain.

Conclusion

Anemic models offer simplicity and are often sufficient for straightforward CRUD scenarios where the domain logic is minimal. They allow you to move quickly without introducing unnecessary complexity.

However, as the domain becomes more central to the application, pushing business rules into services can lead to scattered logic and duplicated validations.

Rich domain models address this by bringing behavior closer to the data, allowing entities to enforce their own rules and maintain consistency.

If you want to check out examples I created, you can find the source code here:

Source Code

I hope you enjoyed it, subscribe and get a notification when new blog is up!

Subscribe

Stay tuned for valuable insights every Thursday morning.