The CAP theorem of domain modeling

Domain model completeness

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

// 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

// 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.

--

--

--

Author of https://amzn.to/2QXS2ch. Founder of https://enterprisecraftsmanship.com/

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Distributed Tracing & Latency Analysis for Microservices

The Difference Between Agile and Scrum

Asteria Weekly Newspaper July9-July16

Tips About Learning How to Program

Accelerate your backend development using AWS + Serverless + GraphQL

April Feature Update: 3MF, POs and more

Building a data-driven sharehouse

Why you should run a 64 bit OS on your Raspberry Pi4

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
Vladimir Khorikov

Vladimir Khorikov

Author of https://amzn.to/2QXS2ch. Founder of https://enterprisecraftsmanship.com/

More from Medium

Collections and Primitive Obsession

Building a Platform: Part 2

CQRS & Event Sourcing I : Introduction

A Beginners Guideline to RabbitMq and MassTransit(Part 2): Implement RabbitMQ in Code with…