DEV Community

Cover image for Domain Services in DDD: Navigating Through Validation and Dependency Management

Posted on • Updated on

Domain Services in DDD: Navigating Through Validation and Dependency Management

Models abstract and simplify reality to make it understandable and computable. By doing so, certain nuances and details are inevitably omitted. Domain models are no exception. The last time we ended up with the following design of the UserProfile class:

public class UserContact
    public EmailAddress Email { get; protected set; } // protected for EF

    public Phone Phone { get; protected set; } // protected for EF

    protected UserContact(){} // protected for EF

    public static UserContact Create(EmailAddress email) => 
        new() { Email = email }; 

    public static UserContact Create(Phone phone) =>
        new() { Phone = phone };

    public static UserContact Create(
        EmailAddress email, Phone phone) => 
        new() { Email = email, Phone = phone };
Enter fullscreen mode Exit fullscreen mode

What if we needed to change the email of the user?

public class UserContact
    // ...
    public void ChangeEmail(EmailAddress newEmail)
        Email = newEmail;
Enter fullscreen mode Exit fullscreen mode

Validation typically takes two forms:

  1. Contextless: Ensures the self-consistency of an object, for example EmailAddress constructor guards implemented one way or another.
  2. Contextual: Verifies an object concerning external factors, like ensuring user email uniqueness across a system.
// UserController
public Task<IActionResult> ChangeEmail(int userId, EmailAddress newEmail)
    // Contextual validation
    User existingUser = await _userRepository.GetByEmailAsync(newEmail);
    if (existingUser != null && existingUser.Id != userId)
        return BadRequest();

    User user = await _userRepository.GetByIdAsync(userId);

    await _userRepository.SaveAsync(user);

    return Ok();
Enter fullscreen mode Exit fullscreen mode

DDD Trilemma: A Crucial Tradeoff in Design

In his thought-provoking analysis, Vladimir Khorikov introduces the concept of the DDD Trilemma, highlighting a pivotal choice developers must make:

  1. Preserve domain model completeness and purity at a potential cost to performance.
  2. Maintain performance and model completeness while sacrificing purity.
  3. Uphold performance and model purity but compromise on completeness.

Making Decisions Based on Queries

Coincidentally, Scott Wlaschin in his book “Domain Modeling Made Functional” (which I strongly recommend even for those who are not familiar with F#) recommends keeping the pure functions intact, but placing them between impure I/O functions if pushing I/O towards application boundaries is not an option because of performance reasons.

Simplifying Decisions: Two Robust Rules

In C#, I found it useful to domain Domain Services should the need arise:

  1. Maintain Purity: use constructors, required keyword accompanied by Value Objects for the “pure” (IO-less) parts of your model.
  2. Domain logic fragmentation is a lesser evil: use domain services for I/O operations, preserving the absence of IO in your entity classes. Introduce a domain service anytime you need the async/await. Don't create async methods in your entity classes. Don't use synchronous IO because it obscures the impurity.

These rules make it so it's easy to choose between an entity method and a service. However, there is one notable exception of the infamous Aggregate pattern, which I'd like to discuss next time.

Top comments (0)