1

I have an entity called User, and it has a method to change the user's email address. I'm using a strongly typed object for representing the email address.

public class User: BaseEntity
{
    // Some other behaviors and state

    public Result ChangeEmailAsync(EmailAddress emailAddress)
    {
        // Validate that a user with a same email does not exist.
        // Change state.
    }
}

If I'm correct, the business logic for duplicate verification and change of email should happen inside thie domain model. And my application layer does not need to check for duplication of emails. But now, I have to call to some data source or service to verify that the user does not change his email into an invalid state.

Then I defined a interface within my domain layer for this task like this:

public interface IEmailExistenceReporter
{
    Task<bool> IsAlreadyExistsAsync(EmailAddress emailAddress, CancellationToken cancellationToken = default);
}

and change the method signature of User entity to accept an instance of IEmailExistenceReporter.

public async Task<Result> ChangeEmailAsync(EmailAddress emailAddress, IEmailExistenceReporter emailExistenceReporter, CancellationToken cancellationToken = default)
{
    var isExists = await emailExistenceReporter.IsAlreadyExistsAsync(emailAddress, cancellationToken);

    // Modify email if not exists, otherwise return failure result with a message.          
}

And this new interface, along with the other repository interfaces will be implemented by the Repository implementation which our domain layer do not need to care about. So I can resolve IEmailExistenceReporter from the application layer using dependency injection and pass it into the ChangeEmailAsync method.

The method seems unit testable too as I can mock the IEmailExistenceReporter. But I feel like, I'm doing something wrong as I'm providing a something like a service into the ChangeEmailAsync method to accomplish its behavior. Is this a bad design or is there a better way to do this?

7
  • I strongly suspect that you need a deeper analysis of your domain; and probably distinct concepts for a submitted email address (information from somewhere else, which may not be unique) and a verified email address (which will be assigned to at most one User at any given time). Commented Jun 19 at 3:24
  • That's rather unusual way to do things. Why can't you have a separate service that both: updates email on user and does this verification? There's no real reason to couple user with this interface to be honest.
    – freakish
    Commented Jun 19 at 6:07
  • @freakish do you mean a service defined within the domain layer which will be called by the ChangeEmailAsync method? Commented Jun 19 at 7:33
  • 1
    @YashojaLakmith you are correct about the first part. Domain objects can be modified outside domain, that's a very common situation. But in my opinion, it is a clean design to not have setters at all. Meaning the object is only constructed with given state, and immutable afterwards. This is not always practical though. So in this particular case the UpdateEmail method would return a new instance of User with new email field. That's how functional programming does things. But you don't have to stick to it, updating the object is fine as well.
    – freakish
    Commented Jun 19 at 9:13
  • 1
    More important thing is that it is beneficial to separate objects that hold data (regardless of whether it is mutable or not) from objects that provide services.
    – freakish
    Commented Jun 19 at 9:14

1 Answer 1

2

Too long to post as a comment, so I'll post it here. As a disclaimer, there are many approaches to solve problems in DDD. This is only how I view it based on how I have implemented things. Absolutely expect to get different answers, which is not to say that any single one is right or wrong.

Ultimately, it is all about context, specifically the bounded context in which you find your aggregate root. Think of it this way, in that your User exists in isolation as a singularity. Your User, then, does not know anything about the Email Addresses of another User, because it is outside of the scope of that single User. As a rule of thumb, if your aggregate requires knowledge of other aggregates of the same type, that knowledge has to come from outside of that aggregate, simply because most of the time, that knowledge requires interacting with the persistence implementation, at some level.

That means that you need something else in your domain which understands that Users, as a pluralized concept, do exist. This is where domain services come into play. From your User domain service, you would ask your repository for the User (or you would create a new User, which I do with a static factory method on the aggregate, personally), set the Email Address on that User, and then persist it back through the repository. There are a couple of points where uniqueness can be checked, with the goal of trying to prevent an error before it makes it to the database. Your repository could expose a method to determine if any User has a specific email address, and then in your domain service, you perform that check before you event attempt to persist the change. The User remains oblivious to the fact that there are other users in the system, because that is knowledge that the User does not have - and why I would strongly advise not to pass magic data into the User, via injection or any other means. This approach is just as testable, if not more so, because your tests will likewise be focused within the context of that user in isolation for the User, and your domain service will be able to test conditions of groups of Users.

Does that mean that you do not need a method to change the email address on a user? It does not. Outside of your aggregate root, the only changes to properties/values inside of your aggregate root should still only be allowed to be made by methods that represent a "task" that the aggregate root allows, based upon your requirements. "The User can change their Email Address", as a simplified example, might be exposed as a method to UpdateCurrentEmailAddress(type emailAddress). It is the responsibility of that method not only to set the property on the User, but also to make sure that the data is valid in the context of that single User. I personally advocate an always-valid domain model, so if this were my project, any methods which modify the User or any of its child entities would return a result-like object that contains a possible error for the caller to inspect (I use a Maybe monad implementation that returns a domain error implementation for this). So, you might check to see if the email is a proper length, confirms to the RFC standards, etc. If it does not, you do not change the Aggregate.

Further that, you could look into encapsulating the Email Address, itself, as a value object. To the domain model, an Email Address is more than just a string value, as it has rules that apply to it beyond its underlying primitive type. That value object then could perform the same types of checks to validate that it is valid only in and of itself. As an aside, for creation operations, instead of a Maybe implementation, I use an Either implementation, because a create operation will either return a domain error if it fails, or the instance of the value object if it succeeds.

2
  • Although my use case requires a domain service, I should put my other business logic with the aggregate root unless they require some outside service, isn't it? Commented Jun 19 at 18:04
  • 1
    Exactly. As a general rule of thumb, try to keep business logic as close to the aggregate as possible, and inside of it, whenever it only applies to that single aggregate in isolation. If you have a Widget aggregate, for example, and it requires its Name to be set, create a method that takes the name, validates the business rules for that name (in isolation), and then applies the value to the aggregate. But, for example, if your Widget requires a unique name, the uniqueness check is done in the domain service, while the isolated validation still stays in the aggregate. Commented Jun 20 at 10:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.