DDD / CQRS / ES - How and where to implement guards

Good morning,

I've a model where a User AR has a specific UserRole (administrator, reseller or client). For that AR, there are some guards I would implement which are:

Let's say that I want to register a new User. The flow would be as follows:

RegisterUser request handler -> RegisterUser Command -> RegisterUser command handler -> User->register(...) method ->UserWasRegistered Domain event

How and where I should implement the guards to validate my User AR exactly? Right now, I've something that look as follows:

namespace vendor\Domain\Model;

class User
{
    public static function register(
        UserId $userId,
        User $manager,
        UserName $name,
        UserPassword $password,
        UserEmail $email,
        UserRole $role
    ): User
    {
        switch($role) {
            case UserRole::ADMINISTRATOR():
                if(!$userId->equals($manager->userId)) {
                    throw new \InvalidArgumentException('An administrator cannot have a manager other than himself');
                }
                break;
            case UserRole::RESELLER():
                if(!$manager->role->equals(UserRole::ADMINISTRATOR())) {
                    throw new \InvalidArgumentException('A reseller cannot have a manager other than an administrator');
                }
                break;
            case UserRole::CLIENT():
                // TODO: This is a bit more complicated as the outer client should have a reseller has manager
                if(!$manager->role->equals(UserRole::RESELLER()) && !$manager->role->equals(UserRole::Client())) {
                    throw new \InvalidArgumentException('A client cannot have a manager other than a reseller or client');
                }
        }

        $newUser = new static();
        $newUser->recordThat(UserWasRegistered::withData($userId, $manager, $name, $password, $email, $role, UserStatus::REGISTERED()));

        return $newUser;
    }
}

As you can see here, guards are in the User AR, which I think is bad. I'm wondering if I should either put those guards in external validators or in the command handler. Another thing is that I should probably also access the read model to ensure uniqueness of user and existence of manager.

And the last thing is, I would prefer pass a UserId VO rather than a User AR for the manager property, hence my thinking that guards should not be put in the User AR.

Your advice would be much appreciated.

Upvotes: 2

Views: 1353

Answers (2)

plalx
plalx

Reputation: 43728

As you can see here, guards are in the model himself which I think is bad. I'm wondering if I should either put those guards in external validators or in the command handler.

With DDD, you strive to keep business logic within the domain layer and more specifically into the model (aggregates, entities and value objects) as much as possible to avoid ending up with an Anemic Domain Model. Some types of rules (e.g. access control, trivial data type validation, etc.) may not be considered business rules by nature and could therefore be delegated to the application layer, but the core domain rules should not leak outside the domain.

I would prefer pass a UserId value object rather than a User aggregat for the manager property

Aggregates should aim at relying on data within their boundary to enforce rules as it's the only way to ensure strong consistency. It is important to realize that any checks based off data external to the aggregate could have been made on stale data and therefore the rule might still get violated through concurrency. The rule can then only be made eventually consistent by detecting violations after they occurred and act accordingly. That doesn't mean the checks are worthless though, as it will still prevent most violations to occur in low-contention scenarios.

When it comes to providing external information to aggregates, there are two main strategies:

  1. Lookup the data before calling upon the domain (e.g. in the application service)

    • Example (pseudo-code):

      Application {
          register(userId, managerId, ...) {
              managerUser = userRepository.userOfId(userId);
              //Manager is a value object
              manager = new Manager(managerUser.id(), managerUser.role());
              registeredUser = User.register(userId, manager, ...);
              ...
          }
      }
      
    • When to use? This is the most standard approach and the "purest" (aggregates never perform indirect IO). I would always consider this strategy first.

    • What to watch for? As in your own code sample, it may be tempting to pass an AR into another's method, but I would try to avoid it to prevent unexpected mutations of the passed AR instance and also to avoid creating dependencies on a larger-than-needed contract.

  2. Pass a domain service to the domain which it can use to lookup data on it's own.

    • Example (pseudo-code):

      interface RoleLookupService {
          bool userInRole(userId, role);
      }
      
      Application { 
          register(userId, managerId, ...) {
              var registeredUser = User.register(userId, managerId, roleLookupService, ...);
              ...
          }
      }
      
    • When to use? I would consider this approach when the lookup logic itself is complex enough to care about encapsulating it in the domain rather than leaking it into the application layer. However, if you want to maintain aggregates "purity" you could also extract the whole creation process in a factory (domain service) which the application layer would rely upon.

    • What to watch for? You should always keep the Interface Segregation Principle in mind here and avoid passing large contracts such as IUserRepository when the only thing looked up is whether or not a user has a role. Furthermore, this approach is not considered to be "pure", because the aggregates may be performing indirect IO. A service dependency could also need more work to mock than a data dependency for unit tests.

Refactoring the original example

  • Avoid passing another AR instance
  • Explicitly model the supervision policy policy as a first-class citizen, associated to a specific role. Note that you could use any modeling variants where the rule is associated to the role. I'm not necessarily satisfied with the language in the example, but you will get the idea.

    interface SupervisionPolicy {
        bool isSatisfiedBy(Manager manager);
    }
    
    enum Role {
        private SupervisionPolicy supervisionPolicy;
    
        public SupervisionPolicy supervisionPolicy() { return supervisionPolicy; }
    
        ...
    }
    
    
    class User {
        public User(UserId userId, Manager manager, Role role, ...) {
            //Could also have role.supervisionPolicy().assertSatisfiedBy(manager, 'message') which throws if not satsified
            if (!role.supervisionPolicy().isSatisfiedBy(manager)) {
                throw …;
            }
        }
    }
    

Upvotes: 2

VoiceOfUnreason
VoiceOfUnreason

Reputation: 57279

Normally - Domain Driven Design calls for rich domain models, which normally means that the business logic is located in methods that represent parts of the domain.

That would normally mean that the command handler would be responsible for the plumbing (loading data from the database, storing changes in the database), and would delegate to the domain model the work of calculating the consequences of the user request.

So the "guards" would usually be implemented within the domain model.

And the last thing is, I would prefer pass a User Id rather than a User for the manager property, hence my thinking that guard should not be put in the User model.

That's fine - when a the domain model needs information that isn't local, you normally either lookup that information and pass it in, or pass in the capability to look up the information.

So in this case, you might be passing in a "domain service" that knows how to look up a UserRole given a UserId.

Are you telling me that it is perfectly valid to pass a domain service to an aggregate? At instantiation level or only to the method dealing with?

My strong preference is that services are passed as arguments to the methods that need them, and are not part of the instantiation. So the entities in the domain model hold data, and collaborators are provided on demand.

"Domain Service" is the third element of the domain model described by Evans in Chapter 5 of the blue book. In many cases, the domain service describes an interface (written in the language of the model), but the implementation of the interface is in the application or infrastructure "layer".

So I would never pass a repository to the domain model, but I would pass a domain service that delegates the actual work to a repository.

Upvotes: 1

Related Questions