The CAP theorem of domain modeling

Domain model completeness

In this article, we’ll talk about a trilemma that comes up in each and every project. To best describe this trilemma, we need to take an example. Let’s say that we’ve got a user management system with one use case so far: changing the user email. Here’s how the User domain class looks:

public class User : Entity
{
public Company Company { get; private set; }
public string Email { get; private set; }

public Result ChangeEmail(string newEmail)
{
if (Company.IsEmailCorporate(newEmail) == false)
return Result.Failure("Incorrect email domain");

Email = newEmail;

return Result.Success();
}
}

public class Company : Entity
{
public string DomainName { get; }

public bool IsEmailCorporate(string email)
{
string emailDomain = email.Split('@')[1];
return emailDomain == DomainName;
}
}
public class UserController
{
public string ChangeEmail(int userId, string newEmail)
{
User user = _userRepository.GetById(userId);

Result result = user.ChangeEmail(newEmail);
if (result.IsFailure)
return result.Error;

_userRepository.Save(user);

return "OK";
}
}

Domain model purity

Let’s now say that we need to implement another business rule: before changing the user email, the system has to check whether the new email is already taken.

// UserController
public string ChangeEmail(int userId, string newEmail)
{
/* The new validation */
User existingUser = _userRepository.GetByEmail(newEmail);
if (existingUser != null && existingUser.Id != userId)
return "Email is already taken";

User user = _userRepository.GetById(userId);

Result result = user.ChangeEmail(newEmail);
if (result.IsFailure)
return result.Error;

_userRepository.Save(user);

return "OK";
}
// User
public Result ChangeEmail(string newEmail, UserRepository repository)
{
if (Company.IsEmailCorporate(newEmail) == false)
return Result.Failure("Incorrect email domain");

User existingUser = repository.GetByEmail(newEmail);
if (existingUser != null && existingUser != this)
return Result.Failure("Email is already taken");

Email = newEmail;

return Result.Success();
}

// UserController
public string ChangeEmail(int userId, string newEmail)
{
User user = _userRepository.GetById(userId);

Result result = user.ChangeEmail(newEmail, _userRepository);
if (result.IsFailure)
return result.Error;

_userRepository.Save(user);

return "OK";
}
public Result ChangeEmail (string newEmail, IUserRepository repository)
public Result ChangeEmail(string newEmail, Func<string, bool> isEmailUnique)

The trilemma

Then why did I call it trilemma and not dilemma? That’s because there’s a third component here, application performance, and sometimes you can give it up in favor of having both domain model purity and completeness.

// User
public Result ChangeEmail(string newEmail, User[] allUsers)
{
if (Company.IsEmailCorporate(newEmail) == false)
return Result.Failure("Incorrect email domain");

bool emailIsTaken = allUsers.Any(
x => x.Email == newEmail && x != this);
if (emailIsTaken)
return Result.Failure("Email is already taken");

Email = newEmail;

return Result.Success();
}

// UserController
public string ChangeEmail(int userId, string newEmail)
{
User[] allUsers = _userRepository.GetAll();

User user = allUsers.Single(x => x.Id == userId);

Result result = user.ChangeEmail(newEmail, allUsers);
if (result.IsFailure)
return result.Error;

_userRepository.Save(user);

return "OK";
}
  • 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.
  • 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.
There’s no single solution that satisfies all three attributes: domain model completeness, domain model purity, and performance. You have to choose two out of the three.
  • Retrieving data from storage
  • Executing business logic
  • Persisting data back to the storage
The best option is when all references to out-of-process dependencies can be pushed to the edges of business operations.
But more often than not, you need to refer to out-of-process dependencies in the middle of the business operation.
  • DDD advocates for this approach because it helps keep the application’s complexity manageable. As you know, DDD is all about Tackling Complexity in the Heart of Software, where “heart” means the domain model.
  • Functional Programming chooses this approach because it’s the only way to make your functions pure. Functional Programming is all about referential transparency and the avoidance of hidden inputs and outputs in the functional core of your application (querying the database, aka database I/O, is one of such hidden inputs).
  • Unit Testing advocates for it because pure domain model means testable domain model. Without the separation between business logic and communication with out-of-process dependencies, your tests will be much harder to maintain as you will have to setup mocks and stubs, and then check interactions with them.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store