Search code examples
cqrsevent-sourcing

How to replay in a deterministic way in CQRS / event-sourcing?


In CQRS / ES based systems, you store events in an event-store. These events refer to an aggregate, and they have an order with respect to the aggregate they belong to. Furthermore, aggregates are consistency / transactional boundaries, which means that any transactional guarantees are only given on a per-aggregate level.

Now, supposed I have a read model which consumes events from multiple aggregates (which is perfectly fine, AFAIK). To be able to replay the read model in a deterministic way, the events need some kind of global ordering, across aggregates – otherwise you wouldn't know whether to replay events for aggregate A before or after the ones for B, or how to intermix them.

The simplest solution to achieve this is by using a timestamp on the events, but typically timestamps are not fine-granular enough (or, to put it another way, not all databases are created equal). Another option is to use a global sequence, but this is bad performance-wise and hinders scaling.

How do you solve this issue? Or is my basic assumption, that replays of read models should be deterministic, wrong?


Solution

  • I see these options:

    • Global sequence

      • if your database allows it, you can use timestamp+aggregateId+aggregateVersion as an index. This usually doesnt work well in the distributed database case.

      • in the distributed database you can use vector clock to get a global sequence without having a lock.

    • Event sequence inside each read model. You can literally store all events in the read model and sort them as you want before applying a projection function.

    • Allow non-determinism and deal with it. For instance, in your example, if there is no group when add_user event arrives - just create an empty group record to the read model and add a user. And when create_group event arrives - update that group record. After all, you have checked in UI and/or command handler that there is a group with this aggregateId, right?