Does a command persist state itself, when we combine CQRS with Event Sourcing, or is it delegating to the EventBus?

48 Views Asked by At

I try to get a better understanding of the corresponding roles of CQRS and EventSourcing regarding persistence especially when both patterns will be combined.

Let's say, we have a CreateUserCommand. The user shall:

  1. be stored into a relational database, which will be used as the base-db for a traditional e-commerce-system (with checkout etc.)
  2. be stored into a special search database (ElasticSearch)
  3. be imported into a CRM
  4. notified about its successful registration

Many blog posts and even some READMEs to corresponding CQRS+ES-Libraries write it like this:

  • the command will be responsible for the main storage and thus store the user in the main database, after that it will dispatch an event UserCreated
  • such dispatched event will additionally be used to write into special search-/query-/read-databases like ElasticSearch to prepare the data for any Read-Queries (the "R"-part from CQRS) and to send the user into the CRM and so on.

However, I doubt that this approach would make the EventStore an effective source of truth. Isn't it, instead, a deviation from the design pattern principles of event-sourcing? But I found so many sources in literature, where Event Sourcing is rather described only as a tool for depicting secondary data sources and not as a primary data source.

I will break down the approach mentioned above, which literature talks about more often and add "my" approach according to my personal understanding, how it should be "done right" instead:

Approach 1:

Command-Side:

CreateUserCommand 
  -> CreateUserCommandHandler
     -> persists the User given in the Command into UserRepository (relational Main-Database)
     -> dispatches UserCreated-event

Event-Side:

Event Bus:
   -> Handler for UserCreatedEvent:
      -> persist the User in the payload also into ElasticSearch
      -> send User-registration-mails
      -> add this user into an external CRM 

To my understanding, at least in theory, the EventBus should be the one-and-only source-of-truth, which eventually means, that any command handler should persist nothing. Instead, they would only dispatch events doing so. According to that, I would see the persistence like this:

Approach 2:

Command-Side:

CreateUserCommand 
  -> CreateUserCommandHandler
     -> dispatches UserCreated-event

Event-Side:

Event Bus:
    -> Event Handler 1 for UserCreatedEvent:
      -> persists the User in the payload into relational main-database
    -> Event Handler 2 for UserCreatedEvent:
      -> persists the User in the payload also into ElasticSearch
    -> Event Handler 3 for UserCreatedEvent:
      -> send User-registration-mails
    -> Event Handler 4 for UserCreatedEvent:
      -> add this user in external CRM 

      (... and many many more)

Which approach is right?

2

There are 2 best solutions below

0
Blafasel42 On

Not sure if there is a dogmatic "right or wrong" in this. I would make it dependent on the scope of the service accepting the data. If this service also has functions to retrieve the record or aspects of it, i would write the projection right away without running it though the event stream first. Only then we send the event. I know one could argue that it is not a by-the-book solution, but on the other hand it makes sure to answer a read directly after the write correctly. In your approach 2, you add latency to updating the service's DB which could lead to irritations later on. In Approach 1 however, reconstructing the projection from the event stream becomes a special case, because "UserCreated" events usually need no processing in the lead service. We implemented an event handler for that, which in normal operations ignores the Event but in "replay mode", processes the events to reconstruct the data from the the stream.

0
VoiceOfUnreason On

Does a command persist state itself, when we combine CQRS with Event Sourcing, or is it delegating to the EventBus?

In most designs that I have seen, the authoritative data store is updated when processing the command.

The EventBus is a strictly optional element.


Many blog posts and even some READMEs

So one thing to be aware of: the Literature[tm] is something of a mess. Martin Fowler introduced the name "Event Sourcing", but what he described isn't quite in alignment with the meaning commonly encountered in the DDD/CQRS/ES community, and those in turn don't necessarily align with what people mean when they talk about event sourcing in, for example, Kafka.

Caveat lector; as a reader you have to be sensitive to the fact that not all authors operate in the same context.


However, I doubt that this approach would make the EventStore an effective source of truth. Isn't it, instead, a deviation from the design pattern principles of event-sourcing?

Context?

In the DDD/CQRS/ES context, the "event store" is going to be some storage appliance that maintains the authoritative sequences of events. That's going to be "the" book of record, with its invariant protected by the logic and policies implemented in the domain-model/command handlers.

(In theory, you could have an event storage that is aware of domain specific contraints to maintain; but in practice most designs assume a general purpose event storage, with specialization in the command handlers.)

The primary capabilities here are going to be centered on the read/write of event histories, and the metadata necessary to identify changes to histories. In particular, we don't expect general purpose query support here.

To support query, there are typically other data storage systems that are optimized for those queries (a relational database, a graph database, whatever), and processes that copy information from the book of record to these other systems.

These other systems are, in effect, caches of information copied from the book of record, with all that that implies (ie: solving cache invalidation).

The EventBus, in these designs, is really "just" a way of moving signals from one place to another, or dispatching transient copies of events for other processing. Anything that needs to see "all" of the events in order will be pulling them from the book of record (normally polling the book of record and batch processing of the data returned).