Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.
Is an anemic domain model a bad thing? Most would probably call it an anti-pattern, and it should be avoided. But is that really true? Well, it depends on what your intent is. Are you trying to create a domain model? Or are you really just trying to create a data model?
Check out my YouTube channel where I post all kinds of content that accompanies my posts including this video showing everything in this post.
In this post, I’m going to be using the example of a food delivery service. You order food from the restaurant and it’s delivered to you.
There is the concept of a shipment which is the delivery of your food order. Every shipment has two stops. The first stop is the Pickup which is the restaurant, and the second stop is the Delivery which is your house/apartment/work or wherever you’re getting the food delivered to.
Each stop (Pickup and Delivery) goes through a progression from being In-Transit, Arrived, and Departed.
The first Stop must go through this full progression before the second Stop can start. This makes sense because the food delivery driver must first Arrive at the Restaurant (the first stop). Once they pick up the food, they depart. Once they have departed the First Stop, then they can Arrive at your house/apartment/work for the delivery. Once they deliver the food, the second stop is now Departed.
Anemic Domain Model
An anemic domain model is an object model that contains a data structure but no behavior. In the case of my shipment example, this means it has an object for the Shipment and the Stops, but no behavior within the objects to make the stop progressions.
While there are a few methods for setting data, this isn’t behavior. We need logic to control the Status on a stop. For example, we can’t change the Stop Status on the Delivery Stop to Arrived if we haven’t Departed the Pickup Stop.
There is no behavior, my object model of Shipment and Stops are just data buckets.
So where does the logic live? In an anemic domain model, generally, you’ll find these in a “Service” or “Manager”. So in this example, you’d likely have a ShipmentService or ShipmentManager that contains the logic.
In the ShipmentService above, we do have logic in various methods for validating that the current state is valid before we perform a state change. Also, note that most of these methods are taking the shipment as a parameter however sometimes I see data access done directly within these methods.
Regardless of where the data access is, the point is that the logic resides separately from the data. The object method of Shipment and Stops are just data buckets and contain no actual logic. All of the logic is separated and contained within inside the ShipmentService which then mutates the Shipment and Stops.
So what’s the issue with this?
It depends on what your intent is. If you’re intent is to make an object-oriented domain model, then this isn’t it. You’ve made an object method that represents the data structure. You’ve made a data model. You think you have a domain model, but you really just have a data model.
Now I suspect most people aren’t trying to create a domain model, what they’re really doing is just trying to create some separation or layering (more on this later in this post). What you’re really closer to are Transaction Scripts.
Entity Services depend on an ORM for data access. The ORM provides returns Entities that are really a one-to-one mapping of a table structure if you’re using a relational database.
Now I actually view most anemic domain models closer to Transaction Scripts than I do a Domain Model. Anemic domain models are kind of this in-between phase of going a bit further than a Transaction Script but not all the way to a Domain Model.
Transaction scripts are much more procedural and often times has many more mixed concerns, such as data access, validation, etc.
The intent of a transaction script is to handle a single request. It contains everything. It would contain the data access to get out the Entities/Domain Objects. It would contain all the validation logic and also state changes.
Because you have many mixed concerns, people want to separate these concerns and land with Anemic Domain Models.
Where this starts falling apart is when you need to duplicate logic or state changes that exist in multiple different transaction scripts. When you start seeing this duplication, you then want to start having transaction scripts call other transaction scripts so you can reuse them and stop duplicating code.
This becomes difficult because if they both contain their own data access logic, then there’s often no simple way of sharing the same underlying transaction.
Once people hit this point, this is where I believe they start going down the Anemic Domain Model path. Instead of duplicating code in different transaction scripts, that’s shared within a single class that turns into the Service (eg, ShipmentService).
The alternative is to have a domain model that exposes behaviors and encapsulates data. Don’t separate the behaviors from the data like you are with an anemic domain model. Keep both behaviors and data within the same object model.
Similar to a transaction script, it can be the one delegating what to do. It can provide the data access to retrieve and/or build our domain model (often an Aggregate Root). It can then call the relevant public methods on the domain model to perform the requested command/action.
The constructor takes a list of Stops. The only way a Stop can be mutated is by calling methods on the ShipmentAggregateRoot. We’re hiding (encapsulating) the data and only exposing behaviors.
To invoke our domain model, we have command handlers that get our Aggregate Root via a Repository, call the appropriate method, then save the aggregate.
Similar to a transaction script, it’s handling a single request, but now instead of having mixed concerns within it, it’s simply delegating to data access and our aggregate root.
Anemic Domain Model
Is an anemic domain model a terrible thing and an anti-pattern that you should avoid? Well, it depends on what your intent is! If you’re trying to create a domain model because you have a lot of complexity, then yes it’s an anti-pattern. However, if you don’t have a lot of complexity that warrants a domain model, a transaction script might be better served for simplicity.
If you’re using transaction scripts and things start getting more and more complex or you start duplicating logic across transaction scripts, then start thinking about building a domain model that hides (encapsulates) data and exposes behaviors.
Developer-level members of my YouTube channel or Patreon get access to a private Discord server to chat with other developers about Software Architecture and Design as well as access to source code for any working demo application that I post on my blog or YouTube. Check out the YouTube Membership or Patreon for more info.