1
votes

I have an email draft as a aggregate root with the following commands: addToRecipient, addCcRecipient, addBccRecipient, updateBodyText, uploadAttachment, removeAttachment and in the UI I want to disable the SEND button if the draft is not ready to be sent (i.e. there is at least on to recipient and the body has text). I know I'm not allowed to query the aggregate but it is the only one that can tell me that I can or can't send the email.

If I am to apply what I know about event sourcing and CQRS, then the aggregate would emit an EmailIsReadyToBeSent event and my UserEmailDrafts read model would pick that and update the UI somehow but then, I would have to check after every command and send a canceling event i.e. EmailIsNotReadyToBeSent.

This feels very complicated, what do you think?

2
Can't you just implement that logic in the read model? The problem with these type of checks is that they aren't state, they are based on state. Trying to persist these checks have it's load of problems, like keeping them synchronized at all times, but it also makes things more complex when you have to change the business logic. For now I just decided to put these rules in the read model e.g. SELECT CASE WHEN some_state THEN 1 ELSE 0 END AS can_be_sent. It's not ideal, but it works. - plalx
One approach I tought of to keep the logic in the domain, but re-use it in the query model is to create specifications that can be converted to abstract expression trees, which in turn could be converted to SQL statements or anything else, but I haven't done it yet. Basically domain.EmailReadyToBeSentSpecification -> domain.Expression -> query.SqlPredicate. - plalx
I have done something similar to what you are saying and it works: those abstract trees are database agnostic and this is a nice abstraction: one could replace SQL with NoSQL quite easy. So, the real question is in fact if this kind of business logic should stay in command model (aggregate) or in the read model. Where is @greg-young when you need him? :) - Constantin Galbenu
But, if I put this logic in the read model then duplication strikes (DIE!) because the aggregate also uses this logic to validate the sendEmail command - Constantin Galbenu
One ideea is to dry execute the sendEmail command - not persist any changes (the aggregate is not saved and the read models are not updated by not publishing the new events) - Constantin Galbenu

2 Answers

5
votes

The fact that an email cannot be sent unless there is a recipient and body is bordering on applicative logic, because at the end of the day it's more a matter of fields being filled in on a form than complex domain invariants.

Rather than relying on a full cross-tier round trip querying the read model each time something changes on the screen, I would inject some knowledge of these basic rules in the UI so that the button is instantaneously re-enabled when recipient and body are specified.

Much like you aren't shocked when you see client-side logic doing required field validation on a form, actually. It's a perfectly valid and accepted tradeoff since the logic is simple and universal.

Note that this doesn't prevent you from having these rules in the aggregate as well, rejecting any command that wouldn't satisfy them.

1
votes

I am going to try to extend the answer given by @plalx with an example of the Specification pattern.

For the sake of the example I am going to use some classes from this ddd library. Specifically the ones that define the interfaces to work with the specification pattern (provided by @martinezdelariva)

First of all, let's forget about the UI and keep the focus on the domain invariants that you must satisfy. So you said that in order to send an email the email needs to:

  • Not contain forbidden keywords.
  • Contain at least one recipient and body content.
  • Be unique, meaning that a similar email was not already sent.

Now let's take a look at the Application Service (use case) to see the big picture before going into the details:

class SendEmailService implements ApplicationService
{
    /**
     * @var EmailRepository
     */
    private $emailRepository;

    /**
     * @var CanSendEmailSpecificationFactory
     */
    private $canSendEmailSpecFactory;

    /**
     * @var EmailMessagingService
     */
    private $emailMessagingService;

    /**
     * @param EmailRepository $emailRepository
     * @param CanSendEmailSpecificationFactory $canSendEmailSpecFactory
     */
    public function __construct(
        EmailRepository $emailRepository,
        CanSendEmailSpecificationFactory $canSendEmailSpecFactory,
        EmailMessagingService $emailMessagingService
    ) {
        $this->emailRepository = $emailRepository;
        $this->canSendEmailSpecFactory = $canSendEmailSpecFactory;
        $this->emailMessagingService = $emailMessagingService;
    }

    /**
     * @param $request
     *
     * @return mixed
     */
    public function execute($request = null)
    {
        $email = $this->emailRepository->findOfId(new EmailId($request->emailId()));
        $canSendEmailSpec = $this->canSendEmailSpecFactory->create();

        if ($email->canBeSent($canSendEmailSpec)) {
            $this->emailMessagingService->send($email);
        }
    }
}

We fetch the email from the repo, check if it can be sent and send it. So let's see how the Aggregate Root (Email) is working with the invariants, here the canBeSent method:

/**
 * @param CanSendEmailSpecification $specification
 *
 * @return bool
 */
public function canBeSent(CanSendEmailSpecification $specification)
{
    return $specification->isSatisfiedBy($this);
}

So far so good, now let's see how easy is to compound the CanSendEmailSpecification to satisfy our invariants:

class CanSendEmailSpecification extends AbstractSpecification
{
    /**
     * @var Specification
     */
    private $compoundSpec;

    /**
     * @param EmailFullyFilledSpecification               $emailFullyFilledSpecification
     * @param SameEmailTypeAlreadySentSpecification       $sameEmailTypeAlreadySentSpec
     * @param ForbiddenKeywordsInBodyContentSpecification $forbiddenKeywordsInBodyContentSpec
     */
    public function __construct(
        EmailFullyFilledSpecification $emailFullyFilledSpecification,
        SameEmailTypeAlreadySentSpecification $sameEmailTypeAlreadySentSpec,
        ForbiddenKeywordsInBodyContentSpecification $forbiddenKeywordsInBodyContentSpec
    ) {
        $this->compoundSpec = $emailFullyFilledSpecification
            ->andSpecification($sameEmailTypeAlreadySentSpec->not())
            ->andSpecification($forbiddenKeywordsInBodyContentSpec->not());
    }

    /**
     * @param mixed $object
     *
     * @return bool
     */
    public function isSatisfiedBy($object)
    {
        return $this->compoundSpec->isSatisfiedBy($object);
    }
}

As you can see we say here that, in order an email to be sent, we must satisfy that:

  • The email is fully filled (here you can check that body content is not empty and there is at least one recipient)
  • And the same email type was NOT already sent.
  • And there are NOT forbidden words in the body content.

Find below the implementation of the two first specifications:

class EmailFullyFilledSpecification extends AbstractSpecification
{
    /**
     * @param EmailFake $email
     *
     * @return bool
     */
    public function isSatisfiedBy($email)
    {
        return $email->hasRecipient() && !empty($email->bodyContent());
    }
}
class SameEmailTypeAlreadySentSpecification extends AbstractSpecification
{
    /**
     * @var EmailRepository
     */
    private $emailRepository;

    /**
     * @param EmailRepository $emailRepository
     */
    public function __construct(EmailRepository $emailRepository)
    {
        $this->emailRepository = $emailRepository;
    }

    /**
     * @param EmailFake $email
     *
     * @return bool
     */
    public function isSatisfiedBy($email)
    {
        $result = $this->emailRepository->findAllOfType($email->type());

        return count($result) > 0 ? true : false;
    }
}

Thanks to the Specification pattern you are ready now to manage as many invariants as your boss asks you to add without modifying the existing code. You can create unit tests very easily for every spec as well.

On the other hand, you can make the UI as complex as you want to let the user know that the email is ready to be sent. I would create another use case ValidateEmailService that only calls the method canBeSent from the Aggregate Root when the user clicks on a validate button, or when the user switches from one input (filling the recipient) to another (filling the body)... that is up to you.