Understanding Domain-Driven Design. From Anemic Domain Model to Rich Domain Model.
#csharp #dddI recently read the book Learning Domain-Driven Design on the advice of my mate, who is more experienced in development than I am. I’ve already written a small review about this book in my Telegram channel (it’s in Russian). Now, I’m going to write a series of articles about how Domain-Driven Design (DDD) can improve your code and software development process. This is the first article in this series.
Anemic Domain Model (ADM)
I’m not sure if I can describe what ADM is better that Martin Fowler. Firstly and foremost, I highly recommend reading his article about this antipattern.
In essence, ADM represents a Domain Model that lacks behavior, consisting solely of a bunch of getters and setters. I created a simple CLI application which uses ADM. The code I have written is based on what I saw during my career as a software developer, and we’ll try to improve it.
The application simulates a system for tracking tickets. It has the following models:
Worker
- a person responsible for resolving the tickets.Ticket
- an entity that describes a problem and can be assigned to any worker.
The functionality of the application allows the user to:
- Add workers.
- Update workers info.
- Fire workers.
- Get workers info.
- Open tickets for workers.
- Close tickets for workers.
Imagine that we have a ticket with Id = 1
and user tries to update Description
of the ticket. To do that, user executes:
dotnet run -- ticket update -id 1 -c "Some new content"
In the result, the ticket will be updated, but what’s going on inside the application? The sequence diagram for this workflow is below:
Figure 1 - Sequence diagram for ticket updating
Seems quite complicated, doesn’t it? What exactly are the disadvantages of this architecture? Here is the list:
- Domain Models and Database Objects are pretty much the same. The DRY principle is violated.
- If you need to implement new functionality, you’ll need to write at least twice as much code.
- If you need to support existing code, you’ll have to spend at least twice as much time on maintanance.
- For each pair of Domain Model - Database Object, you have to create a mapper. More code increases the chances of creating bugs.
- Unnecessary calls to the DB.
- If you need to process hundreds or thousands of entities, you have to load all these DBOs into memory and map them to Domain Models. This will significantly degrade application performance.
How we can improve this existing code? Take a look at the sequence diagram for opening a new ticket:
Figure 2 - Sequence diagram for opening ticket
This example doesn’t have unnecessary requests to the DB, but we still have all other problems that were mentioned earlier. Moreover, you may be wondering where is the BLL? That is one of the problem of such an approach. The business logic is spread across by different kinds of handler. In this particular case, the logic of ticket assigning has leaked to the DAL.
Is it possible to further improve the code? Yes, and this is where a Rich Domain Model (RDM) comes into play.
Rich Domain Model (RDM)
As I mentioned earlier, ADM is simply a class with a collection of getters and setters. However, it’s evident that an RDM is more that just a bunch of getters and setters. It also contains functionality that is directly related to the domain.
To refactor the code from ADM to RDM, we should follow these steps:
- Get rid of duplicated models and mappers by combining Domain Models with DBO.
- Replace
Requests
andQueries
withCommands
andEvents
. - Consolidate all database-related operations from different handlers into a single command handler.
- Transfer domain functionality from handlers to Domain Models.
Get rid of duplications
The first step will be is combine the duplicated models. Lets’do it for Worker
entity. In the initial project, we had Worker
class and WorkerDbo
class as a Domain Model and Database Object, respectively:
public record Worker
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Position { get; set; } = string.Empty;
public bool Fired { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset Updated { get; set; }
}
public record WorkerDbo
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Position { get; set; } = string.Empty;
public bool Fired { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset Updated { get; set; }
public ICollection<TicketDbo> AssignedTickets { get; set; } = new List<TicketDbo>();
}
After refactoring, Domain Model for the Worker will be:
public record Worker
{
public int Id { get; protected set; }
public string Name { get; protected set; } = string.Empty;
public string Email { get; protected set; } = string.Empty;
public string Position { get; protected set; } = string.Empty;
public bool Fired { get; protected set; }
public DateTimeOffset Created { get; protected set; }
public DateTimeOffset Updated { get; protected set; }
public ICollection<Ticket> AssignedTickets { get; protected set; } = new List<Ticket>();
}
This is a crucial and straightforward step. By removing duplicates, we significantly simplified the maintenance of the code. Now, there is no reason to write the same code multiple times and write mappers. Note that all properties have protected set
modifier. Consequently, Domain Model mutations can only be done from within of the class.
Commands and Events
The next step is to replace Requests and Queries by Commands and Events.
A command is a structure which encapsulates an action data that will cause the mutation of the Domain Model. According to the naming conventions in DDD, Commands should be named in an imperative mood plus the Domain Model name. For example, CreateWorker
, UpdateWorker
, OpenTicketForWorker
.
An event is a structure which encapsulates the result of Domain Model mutation. According to the naming conventions in DDD, Events should be named with the Domain Model name plus the verb in past tense. For example, WorkerCreated
, WorkerUpdated
, TicketForWorkerOpened
.
Single command handler
We already have updated Domain Models and also Commands and Events. Now it’s time to implement a Command Handler which will be a single point for all commands in the application. Depends on the type of the Command, Command Handler will call appropriate method for Domain Models.
public class CommandHandler
{
public EventBase Handle(CommandBase command)
{
var @event = command switch
{
TicketCommand cmd => Handle(cmd),
WorkerCommand cmd => Handle(cmd),
_ => throw new NotSupportedException(),
};
return @event;
}
}
Private Handle methods implemented according to one pattern:
- Extract entity from the DB.
- Call appropriate Handle method in the Domain Model
- Save changes in the DB.
- Return result event.
For example, Handler method for Worker
model:
private EventBase Handle(WorkerCommand command)
{
var db = new DatabaseAdapter();
var @event = command switch
{
CreateWorker cmd => db.Workers
.Add(new Worker()).Entity
.Handle(cmd),
UpdateWorker cmd => db.Workers
.First(x => x.Id == cmd.Id)
.Handle(cmd),
FireWorker cmd => db.Workers
.First(x => x.Id == cmd.Id)
.Handle(cmd),
GetWorkerInfo cmd => db.Workers
.First(x => x.Id == cmd.Id)
.Handle(cmd),
OpenTicketForWorker cmd => db.Workers
.First(x => x.Id == cmd.Id)
.Handle(cmd),
CloseTicketForWorker cmd => db.Workers
.Include(x => x.AssignedTickets)
.First(x => x.Id == cmd.Id)
.Handle(cmd),
_ => throw new NotSupportedException(),
};
db.SaveChanges();
return @event;
}
Handlers in Domain Model
Last, but not least, we need to move domain functionality from old handlers to handlers in the Domain Model. The new handlers will be much simpler because now they only contain domain logic without functionality to database. For example, the handler for the UpdateWorker
command looks like:
public EventBase Handle(UpdateWorker request)
{
if (request.Name != null) Name = request.Name;
if (request.Email != null) Email = request.Email;
if (request.Position != null) Position = request.Position;
if (request.Name != null ||
request.Email != null ||
request.Position != null) Updated = DateTimeOffset.Now;
return new WorkerUpdated(this);
}
After refactoring
Now, let’s examine the sequence diagram for the ticket updating functionality and compare with Figure 1 and 2. You will notice that it has become much simpler:
Figure 3 - Sequence diagram for ticket updating using RDM
Actually, the diagram will remain the same for all types of commands, not just for for updating ticket. The only difference will be in Business Logic part.
Conclusion
In this article, I tried to show how DDD patterns and RDM can improve the code base and the architecture of your application. The advantages of using DDD and RDM are the following:
- Encapsulation of data and behavior. Domain Models are no longer just a bunch of getters and setters.
- Understandable naming conventions, e.g. for Commands and Events.
- There is no need to have several classes with the same set of properties and create mappers for them.
- Database calls are kept to a minimum.