11 July, 2019
It’s time for the final part of the article about CQRS and Event Sourcing implementation in PHP. In the previous ones, I’ve presented the idea of Event Sourcing, pointing out its pros and cons. Also, I focused on the implementation of the Write Model and the implementation of the Read Model, using Broadway library. Now, it’s time for the summary of some issues you may face when maintaining or developing an application based on CQRS and Event Sourcing.
The change in requirements
The first issue you may face is the change in business requirements. It can trigger some changes in aggregate. The change may be as tiny as adding or replacing a new field in an object. Let’s assume, that you need to add to the users list the information about the time a user is a member of the group. To do this, you need information about the date of their registration. To adjust the application to the new requirements, there are some changes which need to be executed:
- adding a field with a registration date to aggregate,
- changing an event UserWasCreatedEvent the way it contains additional information about the creation date,
- changing an event handler in aggregate and in a projector.
Now you will face another problem. It’s because you already have some events registered in the Event Store. Unfortunately, these events miss the information about registration date and you need it. Let’s think about what you can do to fix it.
Adding a new event
You can create a new event UserWasCreatedVersion2Event and you can handle both in aggregate. However, it will make you have 2 similar events in aggregate which means more code to maintain. You also have to change the handler of the event UserWasCreatedEvent to make it handle an additional field. How to do it? Well, the easiest way is to set a default value, because there was no such field before.
- events remain unchanged,
- it doesn’t affect an efficiency,
- the complexity of the process doesn’t change.
- it’s necessary to maintain more code in aggregates and projectors,
- it’s obligatory to refactor the handler of a previous version of an event if the changes in the object require that,
- debugging is more difficult because of a lot of events of the same type.
Rewriting existing events in the Event Store
It’s a pretty controversial solution, as the complete history of aggregate is stored in the Event Store and it’s widely known that you can’t change the history. That’s why you shouldn’t modify the events in the Event Store.
- you always have only one version of a certain event,
- it doesn’t affect an application’s efficiency,
- you get rid of unused code.
- you change the history of events,
- you lose the real history of changes irrevocably; you can’t treat the Event Store as an audit log,
- you can make a mistake whilst rewriting events and it may cause some unpredictable results in the future,
- you have to rebuild all the Snapshots.
Upcasting is nothing but upgrading the former events to their newest versions. In the analysed case, you should map UserWasCreatedEvent to UserWasCreatedVersion2Event when reading from the Event Store. Then, when reading the object, you have to pass only the newest version of the event. It will look like on an example below:
Thanks to this solution, you can handle the newest version in aggregate.
- you don’t change the history,
- you maintain the handler of one version of an event in aggregate,
- you get rid of the redundant code.
- you lower efficiency when reading aggregates,
- upcasters need to be maintained which means some additional code,
- you need to rebuild Snapshots,
- no Broadway support.
So, which solution should you choose? It depends on a variety of factors. Personally, I will definitely try to avoid rewriting the Event Store and I would consider it only as a last resort when all the other fails. Upcasting seems to be the most optimal solution, as in domain you don’t have to handle many versions of the same event. In both, reading and writing you have only one version of an event. Also, you can implement Broadway functionalities on your own, knowing that there’s no support from this library. You only have to decorate the existing object of the Event Store or write your own one and then implement the logic responsible for upcasting.
The other problem may be caused by a huge amount of events for single entities. Let’s imagine that the vast majority of the key aggregates contains up to 1000 events. In this case, reading the objects would be extremely expensive, especially if you decide to upcast. This problem may occur if you project your model poorly and the event granulation is too big. Also, it may be that you have too many changes which cause too many events. What to do then? In this situation, Snapshots may be helpful. You just need to save the aggregate status after a certain amount of events. When reading an object from the Event Store, it’s enough to get the newest Snapshot and apply only the newest events. On the example below, there’s a presentation of applying the fifth event to Snapshot.
This solution helps you read the objects pretty fast and quite easily, however it also causes additional issues… As I mentioned above, after providing any changes in aggregates, adding a new version of a certain event and using a proper upcaster, you should also rebuild Snapshots to make sure that they correspond with a current status.
Building an app which is based on CQRS, you mustn’t forget about the fact that propagating events between the Read Model and the Write Model can take a while. Depending on infrastructure and the amount of data – delay may be different. Eventually, the changes will be applied and the models will be compatible. However, you must be aware of it and also – you should make the Product Owner aware that the situation like this will definitely happen and why is it so.
Also, it may happen that during a breakdown, the system will be available but not necessarily up-to-date. Therefore, another question emerges – what is more important for you: system availability or rather up-to-date data. But it’s a topic for another article.
Be responsible when using the tools
You should remember that CQRS is not an architectural pattern. That’s why you shouldn’t use it in the whole system. You should rather set yourself some limits and make sure you respect them. CQRS can certainly solve some problems, but it also brings a huge complexity to your project. That’s why you should consider in which cases it’s actually good to use it and when you should avoid using it as it would only cause more problems.
Before you decide to implement CQRS in your application and before you try to convince your team or client to do so – make sure you test this solution. It’s good to try it on a smaller project. One of the most important decisions related to a project is the events’ granulation. At the same time, it’s extremely difficult to fix after all. Too much granulation will make some events useless as they’ll have too little information. On the other hand, too big events are pretty expensive when serializing/deserializing. Moreover, upcasting may be problematic, because it will be more complicated. That’s why you need to have experienced but also have some instinct to make sure you do it properly. Believe me, you may get these skills through experience and learning from mistakes.
It brings us to the end of this extensive, four-part article about CQRS and Event Sourcing implementation in PHP. Now, you know what are how to use CQRS, Event Sourcing and Broadway library. You are aware of how to implement the Read Model and the Write Model and what are the most common issues you may face when implementing an app using CQRS and Event Sourcing. I’ll advise you to invest some time and try this solution to decide whether or not it’s useful for you. And remember – it’s only worth implementing if you decide that it brings measurable benefits for your project.