Sitemap

Understanding Object-Oriented Design: Rich vs Anemic Models in C#

6 min readMar 17, 2025

--

Press enter or click to view image in full size

As software engineers, we often need to design a Domain Model. There are two completely different approaches the “rich model” and the “anemic model”. One is regarded as good design, while the other is often synonymous with an antipattern. However, is this actually true?

Let’s find out.

Domain

Imagine you are working on a project that represents a note board.

Your task is to model a Note entity in C#. In our system, a Note must have non-empty text that can always be changed. Additionaly, you also need to track the time of the last modification.

This can be designed using one of two approaches:

  • Anemic Model
  • Rich Model

Anemic Model

Usually, people start with the Anemic model, where one class contains the data (model), and another has all the behavior (service).

In code, it would look like this:

class Note
{
public int Id { get; set; }
public string Text { get; set; }
public DateTime UpdatedAt { get; set; }
}

class NoteDomainService
{
. . .

public void ChangeText(Note note, string newText)
{
if (string.IsNullOrWhiteSpace(newText))
{
throw new ArgumentNullException(nameof(newText));
}

note.Text = newText;
note.UpdatedAt = _dateTimeService.UtcNow;
}
}

The anemic domain model has the following advantages:

  • simple to understand: it is straightforward to understand and implement, making it accessible for developers of varying experience levels
  • easy to implement: there is no need for extensive domain analysis beforehand
  • database-aligned design: the model often mirrors the structure of the database schema, making it easier to design the domain with a focus on data first
    — simplified mapping: the same class can be used to represent domain and database object
  • performance. operations are allowed on partially loaded entities, which improves performance
  • DI support: you can inject other services to implement complex logic

However, developers often lack the necessary expertise (or could be just a bit lazy 🙃). Instead of grouping behavior in a separate domain service, they just copy-pasted some logic throughout the codebase.

Get iamprovidence’s stories in your inbox

Join Medium for free to get updates from this writer.

Therefore, this approach is often associated with a lack of encapsulation, which can result in several challenges, including:

  • discoverability: you need to examine all references to understand which properties need to be changed together (e.g., when Text is changed, UpdatedAt must also be updated)
  • no restrictions: nothing guides developers which fields can be changed (e.g. Id field must remain the same during the whole entity lifetime)
  • code duplication: repeated logic across the codebase can appear
  • increased bugs: more opportunities for errors and inconsistencies to arise (e.g., you forget to set UpdatedAt when Text changes)

❗️ Notice:

* Some developers claim that the anemic model violates OOP principles. This is simply just wrong 🙃. You can find various design patterns where data is separated from behavior, e.g. visitor, strategy, command, factory, chain of responsibility, etc.
* Martin Fowler
, Robert C. Martin and other theoretical experts are strongly against anemic model and call it antipattern. Why? Because it would be tough to sell books otherwise 😅.

Rich Model

A rich model is an object-oriented approach where the domain objects (entity) encapsulate both data and behavior.

Which in code is represented by a classical “Tell Don’t Ask” design:

class Note
{
public int Id { get; private set; } // notice: private set
public string Text { get; private set; }
public DateTime UpdatedAt { get; private set; }

public void ChangeText(string newText, DateTime updatedAtUtc)
{
if (string.IsNullOrWhiteSpace(newText))
{
throw new ArgumentNullException(nameof(newText));
}

if (updatedAtUtc.Kind != DateTimeKind.Utc)
{
throw new ArgumentException("The date must be in UTC.", nameof(updatedAtUtc));
}

this.Text = newText;
this.UpdatedAt = updatedAtUtc;
}
}

Encapsulation means:

  • discoverability: you can easily identify which properties should change together without needing to examine all references (e.g., everything inside .ChangeText() method)
  • restrictions: developers cannot initialize properties with invalid values, leading the model to an invalid state (e.g., set empty Text)
  • code reusability: logic is centralized and reused, reducing repetition across the codebase
  • reduced bugs: fewer opportunities for errors and inconsistencies, as the system ensures that related properties are updated consistently

Being the opposite of the anemic model, its drawbacks are also the reverse 😬:

  • complex to implement: it is more challenging for developers, especially those with less experience
  • requires careful analysis: it requires a deeper understanding and analysis of the domain, making it more challenging for developers not familiar with a project
  • domain-centric design: the model is designed around the business logic. It often does not correspond to the database schema, making it challenging to reason about data structure
    — complex configuration. you will need advanced configuration to align your domain model with DB schema
    — additional classes: in some cases, to simplify configuration, you may just introduce separate classes for database tables
    — complex mapping. additional classes lead to a mapping layer. This can be challenging to implement, especially with private set properties and complex hierarchies
  • performance. usually, the entire model and its hierarchy have to be loaded to perform an action. This often affects the performance
  • DI limitation: you should explore alternative ways to implement complex logic in your domain model since it is advised to avoid coupling between an entity and other services

❗️ Notice:

* It does not mean that all behaviour should reside within an entity. You still can have services. It just preferable to encapsulte behaviour when possible.
* You still can have
public properties in rich model as long as they do not break encapsulation.

Choosing Between Rich and Anemic Models

The decision between rich and anemic models isn’t always straightforward and depends on various factors such as the complexity of the domain, team preferences, and project requirements.

Generally, the Anemic model is preferred with simple CRUD, bulk update, and PATCH operations 🙃. Simply because it avoids complex business logic in entities, keeping them as plain data containers, which makes those operations straightforward and efficient.

class NoteAppService
{
. . .

public void Update(int id, string newText, Color newColor)
{
var note = _noteRepository.Get(id);

note.Color = newColor;
note.Text = newText;
note.UpdatedAt = _dateTimeService.UtcNow;

_noteRepository.Update(note);
}
}

The rich model, on the other hand, is less flexible and usually slows down those use cases. It adds unnecessary complexity without much benefit.

class NoteAppService
{
. . .

public void Update(int id, string newText, Color newColor)
{
var note = _noteRepository.Get(id);

note.ChangeColor(newColor, _dateTimeService.UtcNow);
note.ChangeText(newText, _dateTimeService.UtcNow);

_noteRepository.Update(note);
}
}

A rich model is more suitable when you need to enforce invariants, encapsulate behavior, or prevent inconsistent states.

The thing is, when the domain logic is complex, you won’t implement it in the entity but rather in a service.

In general, follow the next advice:

  • If domain data objects are complex and behavior is simple, use the Rich model.
  • If domain data objects are simple and behavior is complex, use the Anemic model.

Сonclusion

An antipattern is a solution that creates more problems than it resolves. While the anemic model is often labeled as an antipattern, in reality, this usually indicates improper usage. The same applies to the rich model — try using it in simple cases, and its flaws will quickly become apparent.

That being said, neither the Anemic nor the Rich model is an antipattern — they’re just often misused 🙃.

My main advice is to avoid the Anemic model in complex scenarios and Rich models in simple cases. If you can easily solve your problem without a Rich model, then you don’t need it. Do not apply it just because somebody said the Animic is an antipattern.

The key here is to understand these patterns and apply them where they make sense.

By the way, few people mention the intermediate option of a pragmatic model where you have behavior but public properties.

class Note
{
public int Id { get; set; }
public string Text { get; set; } // notice: public set
public DateTime UpdatedAt { get; set; }

public void ChangeText(string newText, DateTime updatedAtUtc)
{
if (string.IsNullOrWhiteSpace(newText))
{
throw new ArgumentNullException(nameof(newText));
}

if (updatedAtUtc.Kind != DateTimeKind.Utc)
{
throw new ArgumentException("The date must be in UTC.", nameof(updatedAtUtc));
}

this.Text = newText;
this.UpdatedAt = updatedAtUtc;
}
}

Try it. Maybe this is exactly the solution you are looking for 😉.

Share the article🔄 Clap👏 Support ☕️ And follow ✅ See U 🙌

--

--

iamprovidence
iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence