In a previous post, I described the following pattern: CQRS (marc-architect.hashnode.dev/cqrs-pattern).
Now, I'm going to write about event-sourcing and how it can fit well with CQRS.
Event-sourcing purpose is to persist an object as the sum of all its states.
Instead of storing an object state at a point in time, we will store all the object states change events and deduce the current state as the sum of all the events:
On the left, you always have the last state of the object.
On the right, you need to compute all the states of the object, from the first to the last, to get the current state.
This is event-sourcing and now I'm going to explain why it fits well with CQRS.
I remind that CQRS pattern purpose is to separate (segregate) the reads from the create/update/delete operations to the object (aka. aggregate).
The create/update/delete operations to the object will follow the event-sourcing pattern just described above.
The reads operations will be directed to a view built upon the events and maintaining the object current state (or any other previous state btw).
See the schema below:
The blue squares represent the aggregate creation steps:
# | Aggregate Creation steps | Notes |
1 | The command handler receives CreateObject command | |
2 | The command handler requests to the repository if the aggregate instance exists into the repository | An aggregate is a cluster of domain objects which are considered as one unit with regard to data changes. A transaction within an aggregate must remain atomic. |
3 | The domain repository checks if the aggregate instance is in the cache | not in cache |
4 | The domain repository checks if the aggregate instance is in the event store | nothing in Event Store |
5 | The command handler trigger the invariant verification | Aggregate enforces its own data consistency/integrity using invariants. In case of failure, throw back an exception indicating the business problem. |
6 | The command handler publish ObjectCreated event on the Event bus | This is an internal event bus. |
7 | The domain handler receives the ObjectCreated event from the Event bus | |
8 | The domain handler creates the aggregate instance from the aggregate root | |
9 | The ObjectCreated event is persisted into the Event store | |
10 | The cache is updated with the created aggregate instance | |
11 | The view handlers receives the ObjectCreated event | |
12 | The view handlers update their views |
The green squares represent the aggregate update steps:
# | Aggregate Update steps | Notes |
1 | The command handler receives UpdateObject command | |
2 | The command handler requests to the repository to return the aggregate instance | |
3 | The repository check if the aggregate instance exists into the cache | if yes, go to step 6 |
4 | The repository triggers the rehydration process | compute the current state of the aggregate instance from the Event Store |
5 | The repository puts the rehydrated aggregate instance into the cache | |
6 | The aggregate instance is returned to command handler for invariant verification | then pursue since step 5 from Aggregate Creation steps |
Benefits:
Reliably publish domain events: events are reliably published whenever the aggregate state change (in event-sourcing, objects are built upon events versus events are generated by objects in a traditional way)
Provide an audit log and activity tracing log: all the events can store the identity for audit - they can be consumed by a specific handler to publish them (or part of them) on a message broker for external use
Preserves the history of the aggregate
Drawbacks:
Different programming model
Performance: when a domain object is built upon a huge number of events, there may be some performance issue - solution is to use snapshots (this issue is not present if domain objects are built on relatively small number of events)
Evolving events: schema of events may change over time - so the already stored events do not conform to the current schema any more - solution is to upgrade events to the latest version when they are loaded from the event store (upscaling)
Deleting data: you cannot delete data - solution is to do a soft-delete (emit a Deleted event)
If using a framework, this adds the framework dependency to the service's domain layer
Takeaway:
This pattern is suitable when there is a strong need of activity tracing (at domain object level) and a strong need of domain objects states investigations.
It is also suitable for domain objects that may have various state changes through their life-time.