Some time ago, I wrote an article about a trilemma in domain modeling. It resonated with a lot of people, and I’d like to elaborate on this topic with this email.
In short, the trilemma states that you can’t have all 3 of the following attributes:
-
Domain model completeness — When all the application’s domain logic is located in the domain layer, i.e. not fragmented.
-
Domain model purity — When the domain layer doesn’t have out-of-process dependencies.
-
Performance, which is defined by the presence of unnecessary calls to out-of-process dependencies.
You have 3 options here, but each of them only gives you 2 out of the 3 attributes:
-
Push all external reads and writes to the edges of a business operation — Preserves domain model completeness and purity but concedes performance.
-
Inject out-of-process dependencies into the domain model — Keeps performance and domain model completeness, but at the expense of domain model purity.
-
Split the decision-making process between the domain layer and controllers — Helps with both performance and domain model purity but concedes completeness. With this approach, you need to introduce decision-making points (business logic) in the controller.
Here’s the trilemma in one picture:
In the article, I mentioned that in most applications, you can’t concede performance, and so the decision comes down to the choice between injecting out-of-process dependencies into the domain model (which gives performance and domain model completeness at the expense of purity) and splitting the decision-making process between the domain layer and controllers (performance + domain model purity).
Recently, I received an interesting question which made me reconsider this statement about performance.
We can and often do concede performance, particularly when working with 1-to-many relationships.
For example, let’s say that we have a User
class, and we need to register all its login sessions in our application. A user relates to login sessions as 1-to-many (one user may have multiple sessions).
One way to implement this relationship is make the LoginSession
class part of the User
aggregate. This would allow us to put session-related logic directly into the User
domain class:
public class User { public LoginSession[] LoginSessions { get; } public void RegisterSession(DateTime now) { LoginSession session = LoginSessions.Last(); if (session.HappenedRecently(now)) session.Update(now); else LoginSessions.Add(new LoginSession(this, now)); } } public class UserController { public UserDto RegisterSession(int userId) { User user = _repository.GetById(userId); user.RegisterSession(_dateTimeServer.Now); _repository.Save(user); } }
The downside here is that we can run into performance issues if the number of sessions becomes too large, because we have to load them all from the database even though we need only the last session.
The alternative implementation would be to put LoginSession
into an aggregate of its own and handle the session-related logic in the controller, like this:
public class User { } public class UserController { public UserDto RegisterSession(int userId) { User user = _repository.GetById(userId); LoginSession session = _sessionRepository.GetLast(user); if (session.HappenedRecently(now)) { session.Update(now); _sessionRepository.Update(session); } else _sessionRepository.Add(new LoginSession(user, now)); } }
What are the pros and cons of both solutions?
Both options give you domain model purity, because neither of them refer to the database directly. The difference here is that:
-
With the first option, you get domain model completeness at the expense of performance — You have to load all the sessions but the domain logic remains in the
User
domain class. -
The second option gives you performance but not completeness — We now retrieve only the last session, but the decision-making process is now split between the controller and the domain layer (the controller gets the additional
if
statement).
In fact, this is a common dilemma in DDD, which I mentioned in my DDD in Practice course. The dilemma is about the size of aggregates in your domain model.
The bigger the aggregates, the simpler the code, because it becomes easier to manage transactional boundaries and enforce domain model invariants. On the other hand, the smaller the aggregates, the more performant your application, since each business operation works with a smaller amount of data.
I recommend going with the first approach, as long as the performance impact is not noticeable or not critical for the application. You can always split the aggregates down the road when the performance becomes a problem.
It’s interesting how this performance-simplicity dilemma can be reduced to a more generic performance-completeness-purity trilemma.
--Vlad
Enjoy this message? Here are more things you might like:
Workshops — I offer a 2-day workshop for organizations on Domain-Driven Design and Unit Testing. Reply to this email to discuss.
Unit Testing Principles, Patterns and Practices — A book for people who already have some experience with unit testing and want to bring their skills to the next level.
Learn more »
My Pluralsight courses — The topics include Unit Testing, Domain-Driven Design, and more.
Learn more »