15
votes

Example: Business rules states that the customer should get a confirmation message (email or similar) when an order has been placed.

Lets say that a NewOrderRegisteredEvent is dispatched from the domain and is picked up by an event listener that sends of the confirmation message. When that is done some other event handler throws an exception or something else goes wrong and the unit of work is rolled back. We've now sent the user a confirmation message for something that was rolled back.

What is the "cqrs" way of solving problems like this where you want to do something after a unit of work has been committed? Another complicating factor is replaying of events. I don't want old confirmation messages to be re-sent whenever I replay recorded events in order to build a new view / projection.

My best theory so far: I've just started to look into the fascinating world of cqrs and was wondering whether this is something that would be implemented as a saga? If a saga is like a state machine where each transition only can take place a single time then I guess that would solve this problem? I just have a hard time visualizing how this will fit together with the command bus and domain events..

2
Make the "send email" task a message. If the uow rolls back, so does the dispatching of the message (to durable storage). Something else picks up the message and does the email thingy. Btw, not cqrs related. Plain distributed computing common sense.Yves Reynhout
As for event replay, that NEVER induces this kind of behavior. If your impl. does, you're doing it wrong.Yves Reynhout
That makes sense, but I'm interested in this in a cqrs context. What is a "message"? And who / what would pick it up at, and when? It would be nice to leverage to event store to avoid introducing another storage mechanism to keep track of outgoing communication.Kimble
Your eventstore is your queue. Yet the code doing the actual email sending needs to be notified either by a pull or push based mechanism/technique.Yves Reynhout
@YvesReynhout: That actually just dawned on me a couple of days ago. What I like about that is that it's possible to get rid of any message bus between the event store and the event consumers. This does make each consumer responsible for tracking what it has processed and not, but that's a small price to pay to get rid of a message bus!Kimble

2 Answers

18
votes
  1. An Event should only occur after the transaction has been completed. If anything goes wrong and there's a rollback, then the event didn't occur from an external point of view. Therefore it shouldn't be published at all. Though an OrderRegistrationFailed event could be published if necessary.

  2. You wouldn't want the mail to be sent unless the command has sucessfully been executed.

    First a few reasons why the command handler -- as proposed in another answer -- would be the wrong place: Under some circumstances the command handler wouldn't be able to tell if the command will eventually succeed or not. Having the command handler invoke the mail sending would also put process knowledge inside the command handler, which would break the SRM and too tightly couple business rules with the application layer.

    The mail should be sent after the fact, i.e. from an event handler.

    To prevent this handler from firing during replay, you can just not register it. This works similar to how you test your application. You only register the handlers that you actually need.

    • Production system -> register all event handlers
    • Tests -> register only the tested event handlers
    • Replay -> register only the projection/denormalization handlers

Another - even more loosely coupled, though a bit more complex - possibility would be to have a Saga handle the NewOrderRegisteredEvent and issue a SendMail command to the appropriate bounded context (thanks, Yves Reynhout, for pointing this out in the question's comments).

3
votes

There are two likely solutions

1) The publishing of the event and the handling of the event (i.e. the email) are part of a single transaction. In this case, your transaction framework takes care of it for you. If the email fails, then the event is rolled back. You'll likely retry the command. This is conceptually clean and easy to think about. No event is finished publishing until everyone that has something to say about it has had their say. However practically speaking, this can be painful, as it typically involves distributed transactions. These are hard to come by. Can your email client enroll in the same transaction as the database which is holding your events?

2) The publishing of the event is transactional, but the event handlers each deal with transactions in their own way. The event handler which sends emails could keep track of which events it had seen. If it crashed, it would request old events and process them. You could make a business decision as to how big a deal it would be if people had missing or duplicate emails. (For money-related transactions, the answer is probably you shouldn't allow it.)

Solution (2) is typically what you see promoted in DDD/CQRS circles as it's the more loosely coupled solution. Solution (1) is quite practical in a small system where the event store and the projections are in a single database and the projections don't change often. Solution (2) allows a diversity of event handlers to work in their own way. Solution (1) can cause lots of non-overlapping concerns to become entagled. In this case your order business rules don't complete until the many bizarre things that happen in emailing are taken care of. For one thing, it may slow you down quite a bit.

If the sending of the email were more interesting than "saw the event, sent the email", then you're right, you might have a saga or workflow on your hands. Email in large operations is often a complex system in its own right which you're unlikely to have to implement much of. You just need to be sure you put your email into a request queue of some sort (using approach (2)), and the email system is likely to do retries/batching/spam avoidance/working overnight/etc.