How to Send Emails the Right Way in a CQRS System

Where do you put code for sending emails? Sounds simple right? The funny thing is that if you’re adding it to a CQRS system it can be a little tricky.

Why?

It all depends on when you send them. Too early and other processes may fail and you end up sending your email half cocked. And don’t forget event replay. Could be a bit embarrassing re-sending all the emails since your app launched. I was a hairs whisker away from doing just that once!

So, in a CQRS ES system, where do you put the code to send the emails?

Sending From Within the Domain Model

Sending your message may be an integral part of the domain process but building it into the model is problematic. Any processing from within the domain is before the command handler has committed the changes. This means that the system may have sent the email before for example, generating a concurrency error. This kind of thing will lead to confusion for you and your users.

The other factor to consider is that you do not want to send emails each time you re-run your events. One option is to change your configuration to prevent emails from sending when replaying events. These kinds of questions are indicative of the model having too many responsibilities. It can result in other problems that could emerge downstream of the domain. For these reasons, in most cases, we would need to rule out sending your updates from within the domain model.

What’s Wrong with the Command Handler?

At first glance the obvious place is to put the code is in the command handler. The code could send the email after the handler has committed the new events. Unfortunately this is not without it’s problems. What happens if the send fails? What if the read model update fails? What if the content of the email needs to come from querying the read model? At this stage of the process the denormalisers have not yet received the new events. At this point the read model is inconsistent with the current state of the domain. This also violates the single responsibility principal. The command handler now processes commands and sends emails. Again this poses a problem for event re-play. For these reasons we find that the command handler is rarely the right place.

Option 1: Trigger the Action From the Event Handlers – Just Like a Denormaliser

The event bus is a significant advantage to the application architect. It allows you to add extra discrete functionality into a system without impacting other parts of the system. It acts like a coat rail you can attach new functionality to. In this case, when a specific event enters the bus, we can trigger the notification. This works well when the event has all the data required for the email or SMS. It will even work well if we know that data required is available in the read model. Assuming you do not need to wait for the app to finish processing any of the events this approach will work. This makes replaying events much easier. Just ensure the handler that sends emails is not registered.

This highlights why it is a good idea to ensure each event handler does one thing only. By ensuring each event handler is simple and self contained it allows you to compose responses to event streams. This flexibility to rearrange and adapt the application is a key strength of CQRS ES.

1

2

3

4

5

6

7

8

publicclassSimpleEmailer:IHandle&lt;ClientValueDecreased&gt;

{

publicvoidHandle(ClientValueDecreasede)

{

// Use data provided by the event or look up data from the read model

// Send email ....

}

}

Option 1 is not always going to work for you. You may need data from the read model to populate the email. You may be updating this data from the same event stream. If you were responding to the event stream with all the denormalisers, you would not be able to guarantee the read model would be up to date. Even if you are not reliant on the updates, you may need to ensure all the denormalisers have completed before sending the email. For this you need to turn to a process manager.

Option 2: The Process Manager

The Process Manager is unique within a CQRS system in that it handles events and can issue commands. This is useful when you need to co-ordinate many event handlers and trigger follow on actions. In some applications the message sending infrastructure is a bounded context of it’s own. So to trigger an action the system would need to trigger a command. Even if the message sending system is simple, the process manager can coordinate when to use it.

For example, lets assume that a command generates 3 event messages. Having handled all the events the system should send a completion email. The command handler puts the first event onto the event bus. The process manager receives the message and calls it’s handler. On success it checks if the other events have been processed. When it has completed processing the last event it can then proceed to trigger the completion notification. You can even build in a check to ensure any dependent read model is at the required version before proceeding.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

publicclassLeadProcessManager:

IHandle&lt;HotLeadMarked&gt;,

IHandle&lt;VideoWatched&gt;,

IHandle&lt;EBookDownloaded&gt;

{

privatereadonlyIBus _bus;

privatereadonlyList&lt;int&gt;_callRecord;

publicLeadProcessManager(IBus bus)

{

_bus=bus;

_callRecord=newList&lt;int&gt;();

}

publicvoidHandle(HotLeadMarkede)

{

// Handle event ...

// Transition state

Transition(1,e);// I've hardcoded the state representation for brevity just for this illustration

}

publicvoidHandle(VideoWatchede)

{

// Handle event

Transition(2,e);

}

publicvoidHandle(EBookDownloadede)

{

// Handle event

Transition(4,e);

}

privatevoidTransition(intstate,Evente)

{

if(!_callRecord.Contains(state))_callRecord.Add(state);

if(_callRecord.Sum()==7)SendNotification(e);

}

privatevoidSendNotification(Evente)

{

// Call the email sending system

}

}

N.B. This is an ultra simplistic process manager. It’s good enough for this scenario. I deliberately left out additional concerns like error handling, logging or security for brevity. Process managers in production would often support a wider range of functionality including proper state transitions and even persistence.

How to Avoid Re-Sending Your Emails on Event Replay

I’ve mentioned it earlier but I want to re-iterate it. When replaying your events – DO NOT REGISTER ANY EMAIL SENDING HANDLERS! It’s not good enough to ‘just’ update the config to ensure mails are not sent. Although for belt and braces it is still worth doing. This is because nobbling the config can have unintended consequences. You are likely to build in error handling routines. You may just log exceptions in which case it’s not going to be a problem. It becomes a problem if you start pushing failed messages onto retry queues. Or triggering more advanced error reporting/recovery processes. So while nobbling the config is good for saving face, you do still need to take more steps. The simplest approach is to not register email sending handlers on event replay.

Conclusion

In this short post, I’ve explained why external communication shouldn’t happen within the domain. From breaking the event re-play process to breaking the single responsibility principle. I have also outlined 2 possible solutions. The first is to trigger the action from the event. This assumes no reliance on parts of the read model that may not have completed processing. Option 2 was the process manager. Both of these approaches show how flexible this kind of architecture is and how easy it is to add new functionality.

Join Over 2500 Developers

I hope you found that post helpful. I share more on my email list. I'm learning a lot and so can you. Subscribe today.

I'm a professional software engineer of near on 15 years. Lucky enough to work for a small but rapidly growing company in London called Redington. They have given me the technical freedom to learn some cutting edge technologies like CQRS and Event Sourcing. Now I'm sharing what I learn here.