Event Sourcing: where to put business logic


I can think of two places where to put the domain logic in an event sourced system either one has a drawback.

  • In an event handler of the Aggregate, called locally after creating the event. (That's what I saw in most examples though most of them have very simplistic logic)
    Issue: The event stored in the event store and published to subscribers does not include this processed data and therefore on the projection the same logic has to be applied to the event.
  • Before creating the event. Now the processed data can be stored in the event and the projection doesn't have to know anything about business logic. (I haven't seen this method in examples)
    Issue: In this case though the event only includes the processed data which possibly can lead to information loss.
    Even worse: I also loose the possibility to correct wrong business logic by replaying the events because the event data already has been calculated.

Example: Calculating a metric from some data.
Either I have to calculate the metric twice (one time in the domain model, one time in the projection)
or I have to calculate it before sending the event and including it there.

The flow of control is usually like this:

  • Command is sent to a command handler and properties in the command are pre-validated, like identities are pointing to existing entities and all mandatory information is present and is in correct format
  • Command handler retrieves an aggregate from repository (by reading the event stream, but this is not important) and calls the aggregate method(s) based on what needs to be done by this command
  • Aggregate methods must ensure that their parameters and the aggregate state mutually allow the operation to be performed.

  • Aggregate method then creates an event and calls this When or Apply method to handle the event

  • The event handler only mutates the aggregate state, no logic there!

  • Control flow is then returned to the command handler and it persist all new events in the store

Further actions are related to projections.

The reason to put invariant protection aka business logic into aggregate events, before applying events is because when event is generated there is no turning back. This thing has already happened. You cannot deny applying an event. Think about replaying events when recovering the aggregate from the event stream (reading from repository), how would this possibly work if one day you decide to have an if-throw combination there?

So, in short:

  • Initial logic is applied before the command is sent
  • Some additional logic is in the command handler
  • Aggregate protection in aggregate methods (could very well be also in the command handler)
  • No logic in the event handler, only state mutation

No one ever said that event sourcing would help you fixing issues in your calculations. To make an extra safety net you might want to save commands but then you will have to issue compensating events or truncate streams, which is not really what you would want to do.