15

Let's assume that we want to implement a small security subsystem for a financial application that warns the users via email if a strange pattern is detected. For this example, the pattern will consist in three transactions as the ones depicted. The security subsystem can read events from the main system from a queue.

What I would like to get is an alert that is a direct consequence of the events that happen in the system, without an intermediate representation that models the current state of the pattern.

  1. Monitoring activated
  2. Transaction processed
  3. Transaction processed
  4. Transaction processed
  5. Alert triggered (id: 123)
  6. Email for alert sent (for id: 123)
  7. Transaction processed

Having this in mind, I thought event sourcing could apply here very well, although I have a question without a clear answer. The alert triggered in the example has a clear side effect, an email needs to be sent, a circumstance that should only happen once. Therefore, it shouldn't happen when replaying all the events of an aggregate.

To some extent, I see the email that needs to be sent similar to the materializations generated by the query side that I have seen so many times in the CQRS/Event sourcing literature, with a not so subtle difference though.

In this literature, the query side is built from event handlers that can generate a materialization of the state at a given point reading again all the events. In this case, though, that can't be accomplished exactly like that for the reasons explained before. The idea that every state is transient does not apply so well here. We need to record the fact that an alert was sent somewhere.

An easy solution for me would be having a different table or structure where you keep records of the alerts that were previously triggered. As we have an ID, we would be able to check if an alert with the same ID was issued before. Having this information would make the SendAlertCommand idempotent. Several commands can be issued, but the side effect will only happen once.

Even having that solution in mind, I don't know if this is a hint that there is something wrong with this architecture for this problem.

  • Is my approach correct?
  • Is there any place where I can find more information about this?

It is strange that I haven't been able to find more information about this. Maybe I have been using the wrong wording.

Thank you so much!

0

2 Answers 2

14

How do I deal with side effects in Event Sourcing?

Short version: the domain model doesn't perform side effects. It tracks them. Side effects are performed using a port that connects to the boundary; when the email is sent, you send the acknowledgement back to the domain model.

This means that the email is sent outside of the transaction that updates the event stream.

Precisely where, outside, is a matter of taste.

So conceptually, you have a stream of events like

EmailPrepared(id:123)
EmailPrepared(id:456)
EmailPrepared(id:789)
EmailDelivered(id:456)
EmailDelivered(id:789)

And from this stream you can create a fold

{
    deliveredMail : [ 456, 789 ],
    undeliveredMail : [123]
}

The fold tells you which emails haven't been acknowledged, so you send them again:

undeliveredMail.each ( mail -> {
    send(mail);
    dispatch( new EmailDelivered.from(mail) );
}     

Effectively, this is a two phase commit: you are modifying SMTP in the real world, and then you are updating the model.

The pattern above gives you an at-least-once delivery model. If you want at-most-once, you can turn it around

undeliveredMail.each ( mail -> {
    commit( new EmailDelivered.from(mail) );
    send(mail);
}     

There's a transaction barrier between making EmailPrepared durable and actually sending the email. There's also a transaction barrier between sending the email and making EmailDelivered durable.

Udi Dahan's Reliable Messaging with Distributed Transactions may be a good starting point.

1
  • conceptually, I fully agree with what used, I applied the same in an old project, but now I'm dealing with an AWS serverless setup (DynamoDb, Lambdas) and trying to solve the issue of side effects. DynamoDB Stream does NOT offer filtering, meaning if I want to handle an event with a lambda for this side effect, it will be triggered for every event, then I need to filter the correct events which is NOT practical at all, also having a dispatcher lambda that will filter and publish to a SNS is very SPOF. I was looking into EventBridge but yeah still didn't find anything to be the RIGHT design
    – Ali_Nass
    Commented May 11, 2021 at 18:45
3

You need to separate 'State Change Events' from 'Actions'

A State Change Event is an event that changes the state of the object. These are the ones you store and replay.

An Action is something the object does to other things. These are not stored as part of Event Sourcing.

One way of doing this is with event handers, which you wire up or not depending on whether you want to run the Actions.

public class Monitor
{
    public EventHander SoundAlarm;
    public void MonitorEvent(Event e)
    {
        this.eventcount ++;
        if(this.eventcount > 10)
        {
             this.state = "ALARM!";
             if(SoundAlarm != null) { SoundAlarm();}
        }
    }
}

Now in my monitoring service i can have

public void MonitorServer()
{
    var m = new Monitor(events); //11 events
    //alarm has not been sounded because the event handler wasn't wired up
    //but the internal state is correctly set to "ALARM!"
    m.SoundAlarm += this.SendAlarmEmail;
    m.MonitorEvent(e); //email is sent
}

If you need to log the sent emails, then you can do so as part of SendAlarmEmail. But they are not events in the meaning of Event Sourcing

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.