1

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?

Constantin Galbenu
  • 16,951
  • 3
  • 38
  • 54
  • 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 Sep 22 '16 at 18:26
  • 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 Sep 22 '16 at 18:30
  • 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 Sep 23 '16 at 06:15
  • 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 Sep 23 '16 at 06:25
  • 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 Sep 23 '16 at 07:37
  • That's the beauty of pure functions, like aggregate command handlers are: they can be executed and have no side effects. If they throw exception then the command can't be executed. – Constantin Galbenu Sep 23 '16 at 07:43
  • Yes, these expressions would live in the domain, but can be leveraged by the query model. Therefore, no duplication occurs and business rules are staying in the domain model. – plalx Sep 23 '16 at 12:03

2 Answers2

5

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.

guillaume31
  • 13,738
  • 1
  • 32
  • 51
  • But what if I don't want to send twice a "similar" email (similar means same recipients and same body)? How can I validate this? What comes to my mind is to ask some SentEmailsReadModel, outside any aggregate, but where exactly to put this logic? – Constantin Galbenu Sep 23 '16 at 13:15
  • This is a rethorical question, right? Because in a real-life app, I fail to see what could justify forbidding a user to send a similar email twice. Now if you're talking about accidental double-click on the Send button, I would handle that at the UI level as well - it's basically an input glitch. – guillaume31 Sep 23 '16 at 13:28
  • But what if I don't want to let users send emails that contain some forbidden keywords, taken from an database or external service? – Constantin Galbenu Sep 23 '16 at 13:37
  • And do you want to warn the user along the way as they are building up the command by doing stuff on the UI, or can you afford waiting until they have submitted the command? – guillaume31 Sep 23 '16 at 13:48
  • I warn them, but suppose that I don't trust the UI to not let them press the `Send` button, what then? – Constantin Galbenu Sep 23 '16 at 13:53
  • It depends. Is performance important enough (and when you're talking about on-the-fly validation of text being typed in, it probably is) that the tradeoff is in favor of some duplication between client and server? Are forbidden words really a domain invariant, or more of an application level barrier? – guillaume31 Sep 23 '16 at 14:04
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/124052/discussion-between-constantin-galbenu-and-guillaume31). – Constantin Galbenu Sep 23 '16 at 14:12
1

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.

mgonzalezbaile
  • 1,056
  • 8
  • 10
  • What approach did you use to integrate repositories and specifications? Ideally you do not want to duplicate that logic in querying languages. – plalx Oct 08 '16 at 17:40
  • Sorry @plalx but don't understand well what you mean. – mgonzalezbaile Oct 08 '16 at 22:35
  • For instance, if you wanted to retrieve all email instances that are ready to be sent from storage. You cannot simply load them all in memory to execute the specification. Therefore, you must either duplicate the rule or have a mechanism to translate it to a different language. – plalx Oct 08 '16 at 22:44
  • You're totally right. Honestly I haven't faced that performance problem yet so I can't say an approach based on my experience. I've been reading about the topic but didn't find a solution that convinces to me. Did you have to deal with this? – mgonzalezbaile Oct 09 '16 at 10:10
  • Right now I do duplicate the rules on the query side of things, but I'm thinking of building a mechanism to transform specifications into an AST and then transform the AST into SQL. – plalx Oct 09 '16 at 14:34
  • Sounds good! any way to keep in touch? I would like to see your approach and bring it to PHP, as I said I've not seen anything similar so far – mgonzalezbaile Oct 09 '16 at 14:51