neos / Neos.EventSourcing

A library for Event Sourcing and CQRS for Flow projects.
MIT License
45 stars 30 forks source link

Side Effects: Where to put and how to handle them #24

Closed albe closed 8 years ago

albe commented 8 years ago

Side-Effects are an integral part of nearly every application. Where to put them has therefore become a major concern in current software architectures that allow time-traveling:

http://jaysoo.ca/2016/01/03/managing-processes-in-redux-using-sagas/ https://github.com/reactjs/redux/issues/1528 http://danielwhittaker.me/2015/03/31/how-to-send-emails-the-right-way-in-a-cqrs-system/

I think this is something that we need to discuss and find a good solution for that works with replaying.

albe commented 8 years ago

I'm not so sure about the conclusion to do side-effects in the ProcessManager any more. Either you don't register all your ProcessManagers during replay - then they may for example not emit Events themself (which is a trait I think is quite useful) - or you end up with the same problem as before.

My conclusion currently is one of two things:

bwaidelich commented 8 years ago

What do you mean with side effect, can you provide an example and how you would suggest to solve it?

albe commented 8 years ago

Prime example: Send registration confirmation email

You don't want that to happen every time you replay your aggregate.

As to suggestion, see above, but I'm fully open to other ideas

bwaidelich commented 8 years ago

Thanks for the examples!

registration confirmation email [...] You don't want that to happen every time you replay your aggregate.

I wouldn't send those from the aggregate. And if so, only in the handling part like so:

class SomeAggregate
{
  public function doSomething()
  {
    // validate
   $this->recordThat(new SomethingHasHappened());
   $this->someService->sideEffect();
  }

This is not very error tolerant though, so I usually put those in a separate event listener:

class RegistrationEventListener
{
  public function whenSomethingHasHappened(SomethingHasHappened $event)
  {
     $this->someService->sideEffect();
  }
albe commented 8 years ago

Yeah, Aggregate is actually a big design-error - it totally breaks single responsibility and also might blow up aggregate consistency (in case the side-effect errors out).

So the solution indeed is to have it in some EventListener (or rather ProcessManager as I described it in https://github.com/neos/Neos.Cqrs/issues/8#issuecomment-249173984), but as said above, for those you still need to somehow make it explicit that this EventListener should not be registered during replay.

So you end up with something like PureEventListener and SideEffectEventListener - or ReplayableEventListener and UnreplayableEventListener if you want to avoid the technical buzz words.

bwaidelich commented 8 years ago

make it explicit that this EventListener should not be registered during replay

None of the EventListeners should ever be retriggered. The only thing you can replay is a Projection and that mechanism most probably won't require any bus: It fetches events with minVersion > projection.lastVersion and triggers the projector directly.

BTW: A ProcessManager is only required if it keeps some internal state, probably not for the mentioned example of sending a notification (but I might miss sth)

bwaidelich commented 8 years ago

I close this for now as we have everything in place (except documentation and good example) to deal with side-effects: It's just an EventListener (which can be turned into a Saga/ProcessManager if it needs state)