14

I'm struggling to get my head around what should happen when rebuilding the model by replaying events from the EventStore, in particular when events may trigger other events to happen.

For example, a user who has made 10 purchases should be promoted to a preferred customer and receive an email offering them certain promotions.

We clearly don't want the email to be sent every time we rebuild the model for that user, but how do we stop this from happening when we replay our 10th PurchaseMadeEvent ?

Kirschstein
  • 14,570
  • 14
  • 61
  • 79

3 Answers3

11

Events chaining can be very tricky and easily run out of control, so I'd avoid it as much as possible. For example in the scenario you're describing I'd raise a UserPromotedEvent (maybe even using the PromoteUserCommand), however I wouldn't consider actual/physical sending of an email as part of my domain. Instead I would create additional handler/denormalizer for UserPromotedEvent that would register the need of sending the email with some additional checks quite possibly. After that another process would gather information of not yet processed emails and send them. This approach would mitigate the problems that might occur with not fully accessible/scalable email gateway.

In more general - the need of events chaining very often indicates that you should consider implementing a Saga for the process.

mabi
  • 522
  • 1
  • 6
  • 20
kstaruch
  • 1,289
  • 7
  • 7
8

You should not raise event from event handler - just don't do it! You should use sagas instead.

In your case, saga subscribes for PurchaseMadeEvent and issues PromoteCustomer COMMAND, which causes to happen CustomerPromoted event. Again, there is another saga that subscribes for CustomerPromoted event and sends SendEmailToPromotedCustomer command. When you are replaying events - just don't subscribe saga for CustomerPromoted event.

This is all about the difference between command and event. It is important to understand it. Events tell what already has happened, commands tell what will happen.

xelibrion
  • 2,220
  • 1
  • 19
  • 24
  • I realize this is a pretty old answer but, How would the Saga know to issue a `PromoteCustomer` command when it receives a `PurchaseMadeEvent`? This relies (as per the description of the OP) on the customer having made 10 purchases, which is domain logic which the Saga shouldn't contain. Perhaps Always fire a `TestToPromoteCustomer` and let the aggregate do the check to actually do the promotion? This feels a bit clumsy though.. Thanks for any insight – Geert-Jan Jul 19 '12 at 15:11
  • Yeah, you're right, saga should send TestToPromoteCustomer commands. I think it's absolutely OK to sacrifice purity for maintainability. – xelibrion Aug 07 '12 at 17:25
  • 1
    @Geert-Jan it doesn't need to. the aggregate root makes the decision to reject or process the command. – Pedro Rodrigues Aug 23 '20 at 18:24
3

When you replay events, you're not replaying all of the domain logic that went along with generating those events. Usually in your domain method you'll raise an event; the raising of that event then should update the overall state of that domain object.

For example:

public class Purchase {
  private int _id;
  private string _name;
  private string _address;
  private double _amount;

  public Purchase(int id, string name, string address) {
    //do some business rule checking to determine if event is raised

    //perhaps send an email or do some logging
    //etc.
    if (should_i_raise_event) {
      ApplyEvent(new PurchaseMadeEvent() {
        ID = id,
        Name = name,
        Address = address
      });
    } 
  }

  public UpdatePurchase(int id, double amount) {
    //more checking to see if event is to be raised
    if (should_i_raise_event) {
      ApplyEvent(new PurchaseUpdatedEvent() {
        ID = id,
        Amount = amount
      });
    }
  }

  protected void OnPurchaseMade(PurchaseMadeEvent e){
    _id = e.ID;
    _name = e.Name;
    _address = e.Address;
  }

  protected void OnPurchaseUpdated(PurchaseUpdatedEvent e){
    _id = e.ID;
    _amount = e.Amount;
  }
}

In this example, when my events are replayed, the OnPurchaseMade event handler will get executed, not the domain object constructor. Same with the PurchaseUpdatedEvent - it's event handler will get executed, not the domain method that raised the event.

The event contains everything that you need to update the domain model (and apply the updates to the read model). The domain methods that get executed get you to the point that an event can be raised.

I hope this helps. Let me know if I need to provide more information.

Good luck!!

David Hoerster
  • 28,421
  • 8
  • 67
  • 102