3 and a half heuristics to avoid putting business logic in the wrong place when doing event sourcing
One mistake that is pretty common to people new to event sourcing is putting business logic in the code responsible for rebuilding the state that will serve to make a decision [1]. Any decent introduction to event sourcing will warn people against that, but still, this is a common mistake.
The issue here is that you still need some logic to rebuild the state, and it’s easy to confuse that logic with business logic. We tend to fall into the trap of confusing the two when the computation seems stable and doesn’t change often. After all, why should we put that piece of information in an event when we can recompute it during the rebuild?
I want to offer three ideas that can help distinguishing business logic from state rebuilding logic.
Did a business expert tell you something about that piece of logic?
Is the code that you’re about to write interesting for a business expert? Is this something that you discussed with them? Did they tell you something about that specific rule / number / ... ?
In that case, this is business logic, and it should go in the command part of your system, and the results of any calculation should be stored in an event.
Let’s take two examples. The first one is a common one, the second one comes from a team I’m working with.
The first one concerns tax rates. For now, to get the price with all taxes included, you need to multiply every price by 1.2. Easy peasy, why not include that in the rebuilding state?
Well, what if the tax rate changes? Does this mean that suddenly, all products you sold with a 20% VAT were actually sold with a 10% VAT? Of course not!
The reconstruction phase will have to include some more logic to deal with the tax rate change, such as figuring out the right tax rate based on the date of the event.
The second example concerns an expiration date. Imagine that buyers can add items to their cart for 20 days after the basket was created. Since we know the date of the BasketWasCreated
event, we can easily compute the expiration date in the reconstruction phase. This is as simple as taking one date and adding a duration.
But here again, what if the business experts come to you and ask you to change the duration, reducing it from 20 days to 10 days? Should you say no to all users who believed they could act after 20 days, only after 10 days, now that the rule has changed? Probably not.
These rules come from our business experts. They told us about the tax rate and duration. The computation should go into the command part of the system, and the result should be stored in the event.
With the two examples, we also saw that other interesting questions to ask yourself are: Is it possible that this business rule might change at some point? And if so, what is the impact? But as I said in the introduction, the trouble starts when we believe that the logic is stable, which makes this guideline less interesting than just looking at who told you about that logic.
Let’s move to another idea.
It should be about state representation, nothing more.
Now that we’ve seen a heuristic for detecting what should go in the handling command part of our event-sourced system, let’s look at what should go in the reconstruction phase.
The reconstruction phase consists of applying all past events to rebuild a state that will help to make future decisions. This is nothing more than a particular live projection.
The logic in the rebuild state should only be about data structures. If you can find multiple ways to represent the information you are probably on track.
For instance, let’s imagine that we have a (probably stupid) business rule that prevents people from buying more than 10 products at once. In the rebuild phase we could take the number of products added in the basket when we apply each ProductAddedToBasket
event and sum that, having only an integer in the state. Alternatively, we could rebuild the entire basket with a collection of all products and use products.length
when we handle the command to add a new product to the basket.
We found two ways of representing the state. And, by the way, business experts don’t care which one we use. This is clearly reconstruction logic.
Are you doing computation on one event only?
Another idea that can help prevent the mistake of putting business logic in the wrong place is to look for areas where computation is being performed in the rebuild phase based on data contained in only one event.
Let’s retake all previous examples:
- To compute the prices, including taxes, we would only need data from one
ProductAddedToBasket
event. We are not making calculations across multiple events, just from one. - To get the expiration date, this is the same thing. We are only doing calculations using data coming from the
BasketStarted
event. That logic should go in the command handling part of the system. - To have the entire quantity of products added to the basket, we need to look at multiple
ProductAddedToBasket
events. This looks good, and the logic belongs to the rebuild phase.
Do you speak French and want to stop hating your tests ?
I've created a course to help developers to improve their automated tests.
I share ideas and technics to improve slow, flaky, failing for unexpected reason and hard to understand tests until they become tests we take joy to work with !
But you'll need to understand French...
Be careful; this heuristic comes with some limitations. Depending on how you represent the information in the events, you could need to do some computation based only on the data from one event.
For instance, you could store the tax-free price and the tax rate in the ProductAddedToBasket
event. In that case, you would need to recompute the price, including VAT, based on these two pieces of data.
The same is true for the other example. Instead of storing the expiration date, you could store the start date and the duration before expiration.
As we can see, this is not a definitive rule; it is just something to look for.
That heuristic could be refined to watch out for logic using data not coming from events.
Or, in yet another form: Be very suspicious about every constant value in the rebuilding logic.
Look for hardcoded values, even hidden behing constants and ask you if it that could go in the command handling part.
We probably can find other clues that we are performing business logic in the rebuild state. These are mine - I might even have other unconscious ones; who knows? - and I hope that they will be useful to you.
If you would like to explore building an event-sourced system further, are struggling with an existing one, or think you could use a hand, let’s chat.
When rebuilding the aggregate, if you will. ↩︎