We’ve all encountered anemic domain models, but what are they exactly?
Those models only contain the data (accessed via getters/setters), but the behavior is stripped from them. In that case, we have a model class, and another class (typically a stateless service class) that handles everything related to the behavior of that class (see Figure 1).
Figure 1. Anemic domain model
A contrast to anemic domain models would be rich domain models. These models combine data and behavior (see Figure 2).
Figure 2. Rich Domain Model
From an object-oriented programming perspective, rich domain models are more in compliance with fundamental principles of OOP. They follow this mantra: Give the responsibility of keeping the data integrity to the class holding the data.
Anemic Domain Models
Let’s return to the anemic domain models. Some of the problems we encounter with them are:
- Discoverability – It can be hard to understand what the capabilities of that domain model are if it’s stripped from its behavior. That can cause code duplication because some new developers might not find a service that does the operation they need.
- This can be addressed by applying coding standards (project organization) so that a person knows where to expect a service class.
- Lack of encapsulation – You must expose your entire model for service to be able to work with it.
- Encapsulation is all about protecting data integrity. We hide the data and keep handling it together with the data itself.
- Lack of encapsulation leads to higher complexity, which then leads to all kinds of troubles like
- Harder maintainability of the code
- Slower development
- More bugs
Even though we just spoke about issues with anemic domain models, they have their place in software design. Here are some advantages of anemic domain models:
- It’s easy to understand. Suppose you’re thinking about how you would store your data in a database. In that case, it becomes very intuitive to create a class that maps to your database schema.
- Easy to implement. You can start quickly because you must think about the data, not the behavior right away.
Although, as the complexity of the project rises, so does the difficulty of maintaining a code with anemic domain models. If you’re building a small project, a rich domain model might be overkill. Another pitfall of anemic domain models is that you might feel tempted to use the domain model for all layers (data, domain, controller). Then it becomes challenging to make any changes to your model. If, for example, you need to change something in your database, you must be careful because it directly affects your API endpoint. By the way, some controller templates in IDEs (like Visual Studio) use your domain model as a data contract, so be wary of that.
Rich Domain Models
We mostly spoke about anemic domain models up until now. Let’s now jump on rich domain models. As mentioned previously, rich domain models are self-sustained models. We can implement proper encapsulation since we keep both data and operations in the same class. Effects of using a rich, fully encapsulated model:
- All operations upon domain classes are in themselves
- Domain class cannot be in an invalid state
Now we will try to express differences in an example. We are building a simple Note-taking app. Requirements:
- We want to be able to
- add a note (plain text)
- update a note
- order by date created
- order by date updated
- Cannot save an empty note
Let’s try to do it with anemic domain models. We will have a domain class Note:
We can notice that all properties are public for both getting and setting the value. We must have it open like that (or at least internal visibility) for the service class to be able to handle it. Now we can show a NoteService class:
The problem with this solution, as mentioned before, is the lack of encapsulation. We could instantiate Note directly, and set a DateCreated to a future date, or DateUpdated before DateCreated; we could even update the ID. That puts unnecessary pressure on the developer to know exactly how to use our class or that they should use the NoteService class.
Now let’s do the same example with the rich domain model. The domain model would look like this:
Notice that all properties have private setters. This ensures that we cannot get this model into an invalid state. The only way of setting properties is through the constructor or SetText method. Users of this class cannot choose DateCreated or DateUpdated because those fields are implicitly set. Another thing to note is that we are also checking if the input text is null or empty, and throwing an exception in that case. That validation is tied to the Text property. For this simple example, that is totally fine. But we could improve this solution by introducing a value object instead of a plain string type. Now let’s define value objects.
Value objects are entities that encapsulate part of your domain model. They have several traits:
- They have no identity – they have no unique identifiers
- They are immutable
- They are interchangeable – two value objects with the same property values are considered the same
- Their lifetime is bound to the lifetime of the encompassing domain model
Let’s now create a note text value object.
Suppose we ever want to add additional constraints to the note text. In that case, we can do it in NoteText class (e.g., add a maximum number of characters). One important note to add here: we can encapsulate more than one field in a value object. Those are typically mutually dependent fields. A typical example of this is a price (imagine an e-shop with items) comprised of two components: amount and currency. That is an ideal candidate for a value object.
Now we can update the Note class with NoteText in place.
Our domain class is now clean and protected from an invalid state. We didn’t wrap DateCreated or DateUpdated with a value object since these fields cannot be changed from the outside. In other words, they are under the domain model’s control.
We have seen how we could implement a simple solution with both anemic and rich domain models. Both solutions will work, but their maintainability is vastly different. Anemic domain models allow for bad programming practices, which can cause painful bugs. Rich domain models need some getting used to and might seem clunky at first, but the benefits outweigh the steeper learning curve in the long run.