---
title: 'Stop Sacrificing Performance for ''Pure'' Domain Models'
source: 'https://youtube.com/watch?v=P1wAvBKriVs'
video_id: 'P1wAvBKriVs'
date: 2026-08-08
duration_sec: 640
---

# Stop Sacrificing Performance for 'Pure' Domain Models

> Source: [Stop Sacrificing Performance for 'Pure' Domain Models](https://youtube.com/watch?v=P1wAvBKriVs)

## Summary

The video explains three key concerns in .NET domain model design: purity, completeness, and performance. It demonstrates through a to-do example how you can only achieve two of these at a time, and discusses the trade-offs of each combination.

### Key Points

- **Three concerns defined** [00:01] — Introduces the three concerns: purity (no out-of-process dependencies), completeness (business logic encapsulation), and performance (implementation efficiency).
- **The impossible triangle** [01:07] — The premise is that you can never have all three at once; you can only have two.
- **Pure and complete implementation** [03:41] — Pure and complete: enforce business rules by passing a read-only list of existing to-dos, but this loads all entities into memory, hurting performance.
- **Complete and performant implementation** [05:39] — Complete and performant: use an external service (high priority to-do counter) to check the count, improving performance but making the model impure.
- **Pure and performant implementation** [07:29] — Pure and performant: move the business rule check to the application layer, keeping the domain pure and fast, but splitting logic.
- **Choosing the right trade-off** [08:11] — For most applications, the pure and performant approach is ideal because performance is rarely worth sacrificing. The choice depends on context.
- **Handling race conditions** [09:17] — Race conditions can be handled with optimistic or pessimistic concurrency, or by failing the request.

## Transcript

inside of your .NET applications, there are three concerns that you should be aware of when it comes to how you design your domain models. These concerns are domain model purity, domain model completeness, and performance. I'm going
to explain what the three of these are and how they impact your domain model design. I first encountered this idea of purity, completeness, and performance in an article from Vladimir Khorikov, which I'm going to leave in the description of
this video. Let me briefly explain what the three of these means. Domain model purity concerns with if your domain model references out of process dependencies. If it doesn't have any out of process dependencies, then we
consider this a pure domain model. Domain model completeness deals with how much of the business logic is encapsulated within your domain model. of your domain models, then you've got what's called a complete domain model.
If some of it is split between your domain model and application services, model. And performance, I think, is self-explanatory. It just cares about implementation. However, you will see how this forces us to either trade off
purity or completeness. Now, if we draw an intersection of these three, so this will represent purity, this will represent completeness, and we've got another one for performance, the premise is that we can never have all three of
them at once. So, this intersection here is not attainable within your domain model. However, at any point in time, you can have two of these. So, just let me copy these inside. So, you can either have purity and completeness, or purity
and performance, or completeness and performance, but you can't have all let's look at a concrete example to understand why this is the case. I've got a use case for creating a to-do item inside of my application. And the domain
model right now is doesn't really have any logic inside, However, there is a new rule that we want to enforce with our domain model, and that is being able to have at most five or a configurable value of active
high priority to-dos. Now, we classify this based on the priority enum, and high priority to-dos are either going to have a high or a top priority. So, we want to somehow enforce this through our domain model, and we're going to view
completeness, and performance. Now, I'm going to create a copy of our create to-do command handler and implement the first version in there, and let's look at a pure and complete version. I'll also rename the file to match the type
original untouched and implement our logic in here. So, let's start by creating some sort of factory function inside of our domain model. So, I'll say I'll create a static function that returns a to-do item. Let's call it
create. And what do we want to do inside? We want to have this logic that we have here inside of our domain model, so I can just drop it here. Let's return the to-do item. And now we need to provide these values as arguments. So,
that's going to look something like this, and I'll need to just reference these when setting the to-do item properties. We'll update our use case to get something like this. So, now we pushed the logic down. We haven't
completely encapsulated it because there's still a public constructor. We could fix this by creating a private parameterless constructor, for example, so I'll just comment this out and leave a brief descriptive comment. But, let's
focus on our new business rule of enforcing a maximum amount of high priority to-dos. So, remember that we want to make this pure and complete, which means that we can't have external references inside of our domain model,
but we also need to enforce our business rule within the domain model itself. So, how we could enforce this, for example, by accepting a read-only list of to-do items, and we're going to call this existing high priority to-dos. And what
we want to do here is basically check that if the current priority is either high or, let's say, top, and the existing high priority to-dos, or rather the number of them, is greater than or equal to the maximum allowed number of
high priority to-dos, then we want to return some sort of failure results. Now, I have to refactor this to return a result object, and in this case, we're going to return a result failure containing our to-do item, and we're
going to say to-do item errors, high priority limit reached. And now we've encapsulated this business rule inside of our domain, and we don't have external references, so this is both pure and complete. Now, I have to
somehow provide this list here to be able to satisfy our contract, and the simplest way to do this is a query like this using EF Core, where we're going to fetch the to-do items for this user that aren't completed and have a priority of
either high or top. And now I can pass these in to the create method. Now, what we are sacrificing here is performance because we are loading all of the to-do items into memory. And this is kind of the worst-case scenario. Imagine when
that we have to validate. This wouldn't give us ideal performance in a production environment. So, that's the pure and complete implementation. Now, let's make another copy of this, which I'm going to go into and rename, and
let's call this complete and performant. The idea here is that we get performance by not loading all of the entities into memory while still keeping completeness.
So, we have to delegate our business check to some sort of external service. for us. I'm going to define this inside of our domain project in the to-dos folder, and I will call this abstraction a high priority to-do counter. This is
just an interface, and you can already guess that this introduces an external dependency. I'll create another version of the create method, which is going to have an additional argument being our high priority to-do counter. So, this is
what makes our new implementation impure, and since this is an async a cancellation token. So, what's going to change here is if the priority is high or top, we're going to get the count by calling the high priority to-do
counter, which exposes one method. We'll pass in the user ID and the cancellation token. I will have to make the entire thing asynchronous by returning a task. And then, if the count is greater than or equal to the maximum number of high
priority items, then we return a failure result. So, now we have to use this inside of our application layer. So, we go back to our use case. We'll inject this as another dependency, so high priority to-do counter, and we're going
to call this overload, which accepts a high priority to-do counter and a cancellation token. We no longer have to load our high priority to-dos into call our create method, which is now still complete because our domain logic
but now it's impure because we are referencing an out of process dependency have significantly improved performance because we are only counting how many of these to-dos are there in the database. The implementation for this will be in
there could be a high priority to-do counter service, which implements an interface, and it does the same query, except it's now a count. And we also have to wire this up with dependency injection so that we can use it within
our use case. So, that's the second option. And let me give you the third what would be the most optimal one. So, I'll create another copy of this, and we're going to rename this to be pure and performant, but incomplete. I'll get
rid of the high priority to-do counter, and instead, I'm going to move the logic of checking the number of high priority to-dos into our application use case. That means I'll need another overload of the create method, which is going to be
the simplest one that doesn't have any references to external services or anything like that. It just creates a to-do item and raises a domain event. calling here. Let's get rid of the cancellation token and the to-do counter
performance solution. We are only doing the minimal amount of work to check this advanced business rule while keeping the domain pure as it doesn't have out of process dependencies. Our business logic is now split within the use case and
the domain. And for the majority of applications out there, this will probably be the ideal solution because performance is something I don't want to trade off ever. So, going the pure and complete route rarely makes sense for me
in a production setting. And then, depending on the situation, I could consider if I want to use an impure version and have most of the logic inside of the domain layer, but this only makes sense if the business logic
itself is somewhat simple. Like this one check here, we'll probably be fine with this. It also improves testability because you can now test the complete use case only through the domain without having to touch the application layer.
However, a lot of people are going to be uncomfortable with this as you are the domain model. So, where most people land on is just doing their checks in everything is said and done, and we've made sure that we can run our domain
delegate to the domain layer to do the remaining work. I know some of you are going to comment on the race condition here. There are ways to get around this either with optimistic concurrency by having a version that we checked on the
failure, or we could even completely fail the request, or we could go for the pessimistic concurrency and introduce a strict lock for this use case whatever the result of this evaluation here for checking the number of high priority
to-dos remains true by the time we get to call save changes, and we always keep a consistent state in the database. And having to deal with things like this is also what makes it difficult to keep the domain model pure. If we had to deal
with database concerns and race conditions inside of the domain model, then that would really become a big mess really fast. However, keeping these us to keep our domain model simple. So
that is how purity, completeness, and performance can impact how you design your domain models. Now, let me know in the comments which of these options you would choose. I always lean in favor of performance, and then I would pick and
choose between also having purity and completeness, but I do this on a case-by-case basis, and I don't really have a strict rule for what should be decisions should be made based on the current context within the application
that you are building. If you want to grab the source code for this video and they're going to be available in the pinned comment right below. If you enjoyed this video, consider gently tapping the like button to let me know.
Thanks a lot for watching, and until next time, stay awesome.
