Oskar Dudycz

Pragmatic about programming

Anti-patterns in event modelling - State Obsession

2021-09-15 oskar dudyczEvent Sourcing

cover

Some time ago, I tackled the first event modelling anti-pattern: “Property Sourcing”. Today I’d like to tackle the next one: “State Obsession”.

I have already written that Bank Account this is not the best example, but let’s try again. Let’s assume that we would like to model financial transactions. We may have different transaction types: cash deposit, debit card payment, foreign or crypt currency, even a check. In the end, the most noticeable effect is the change in account balance. Deposits increase it; withdrawals decrease it.

We may conclude that we could do a BalanceUpdated event that would store information on how much the balance changed. Positive value for deposits, negative for withdrawals, e.g.:

BalanceUpdated {Amount: 50}

BalanceUpdated {Amount: -50}

BalanceUpdated {Amount: 100}

BalanceUpdated {Amount: -20}

We could use those events to calculate the total balance (80 = 50 - 50 + 100 - 20).

Such event structure can look like a reasonable idea. We made our processing generic and simple, right? However, by trying to standardise processing, we flattened business information. Ignoring the specifics of those operations and not creating detailed events like DepositRecorded, CashWitdhrawnFromATM, TransactionVoided, we’re risking the loss of important business information. Events are business facts and should be focused on the outcome of the operation. Ultimately, withdrawing cash from an ATM will have different properties than paying by a credit card. When modelling events, it’s important not to think about the state change but about what exactly has happened. We should keep things simple but not oversimplified.

Therefore, in my opinion, it is better to replace the generic event with more specific ones, even if they seem very similar at the design time. It is much more valuable to have this sequence instead of the previous one:

DepositRecorded {Amount: 50}

CashWithdrawnFromATM {Amount: -50}

IncomingTransferRecorded {Amount: 100}

CreditCardPaymentMade {Amount: -20}

Just by looking at them already, we can see what was happening.

An even worse problem would be if we modeled the BalanceUpdated event as storing the current account balance after the transaction was registered, for example:

BalanceUpdated {Balance: 50}

BalanceUpdated {Balance: 0}

BalanceUpdated {Balance: 100}

BalanceUpdated {Balance: 80}

There is no information about the type of transaction. We also lost the only specific information: transaction amount. We need to know the current and previous balance to be able to calculate the transaction amount.

In the case of these events, the specific information is that you deposited 50$ at the bank and then withdrawn them at the ATM. In the case of the transaction processing, the account balance is derived information. It’s not specific for the transaction that happened but calculated based on them. We should not overlook critical details. It is the direct way to losing data. If the algorithm or taxes change, how will you know what the transaction amount was?

Of course, life is not only black and white. Sometimes we need to make pragmatic decisions. For instance, we must remember that the most common use case for events is (re)building read models. Projections that are used for that should be relatively simple code, without much business logic. In our sample case, we would not like to duplicate the logic of calculating the account balance in many different places. Especially since they don’t have to be so trivial as subtract here, sum there. The calculation might get complex with taxes, bank commissions, currency conversions, etc. We should try to keep such business logic on the write model. Sometimes it’s worth adding a little redundancy to our events. In our sample case, it could be adding an information about the current balance. They could look like this:

DepositRecorded {Amount: 50, Balance: 50}

CashWithdrawnFromATM {Amount: -50, Balance: 0}

IncomingTransferRecorded {Amount: 100, Balance: 100}

CreditCardPaymentMade {Amount: -20, Balance: 80}

It gives us several advantages:

  • we maintain the business logic in the write model. Read models do not have to be aware of it.
  • we have frozen information about the balance, even if the balance calculations (e.g. taxes) change. Thanks to that, we can avoid the need to version the calculation formulas.
  • generating the reading model is more manageable, and more importantly, easier to deal with idempotency. If we have an ordering guarantee, applying the event multiple times will have the same effect. If we only have the transaction amount, we must have a dedicated mechanism to avoid charging the same transaction multiple times.

This not only apply to read models, but equally about any downstream consumers. Events can trigger other workflows as well (e.g. fraud detection, etc.). They don’t have to be only to update the reading models. Events are facts. How they are interpreted depends on the subscribers.

It is a bit grey area when to allow redundancy and where not. You have to analyse the pros and cons each time. We should avoid putting a derivative state in our events (e.g. balance) when possible. We should try to keep them as concise and small as possible, but not smaller.

We can pragmatically add additional information or make transformations to help subscribers, but that should not be the basis of our considerations. It should be an optimisation that we have carefully crafted.

In an event-based approach, we don’t have to think about everything at once. First, we should focus on how to preserve a business fact best, then how to use it. We can create new read models based on already existing events. As long as we register all the essential information, we will be able to use it later. If we follow these rules, we don’t even have to design read models while working on the business logic. Primarily, by combining the read model with the event model, we lose the autonomy of these models.

One of the most significant benefits of using Event Sourcing is not losing data. We should focus on recording all information related to the business operations results. Read models or other modules should take it from there.

To get it right, you have to change your mindset of thinking “what has changed?” to “what has happened?“.


If you’re dealing with such issues, I’m happy to help you through consulting, training or mentoring. Contact me and we’ll find a way to unblock you!

See also more in series about event modelling anti-patterns:

Check also more general considerations:

Cheers!

Oskar

👋 If you found this article helpful and want to get notification about the next one, subscribe to Architecture Weekly.

✉️ Join over 11500 subscribers, get the best resources to boost your skills, and stay updated with Software Architecture trends!

Loading...
Event-Driven by Oskar Dudycz

cover

Through my window, I see the result of good plans but poor execution. Opposite my flat, there is a partially completed construction place. Buildings were supposed to be eye-catching Mediterranean style apartments. Delivery date? Two years ago. Actual? More and more unknown.

Some time ago, I heard that using Event Sourcing makes creating Event-Driven Architecture easier. The arguments were correct, that if we’re already publishing events to trigger business workflows, then at some point, we may want to also store events to not lose information. Agreed. However, I also heard that keeping the state as events will simplify things. We’ll have a source of truth with a record of the system behaviour. This will allow, e.g. to confront the results of the operations with the recorded state. I’d agree with that, with one distinction. It’s easier as long as you already know Event Sourcing.

Many people in the DDD community claim that the essential is to properly break down the system into autonomous parts called bounded contexts. Once we have it, the rest is secondary and will sort itself out. For sure.

Many seasoned programmers speak similarly about new technologies. They claim that they can translate past experience into new technologies. That’s true that by analogy, they can catch the big picture quicker. But isn’t it a bold assumption to say that Win.Forms specialist will learn Angular quickly?

The end result may differ a lot from the initial ideas. I saw the plan of those buildings next to me. Now I can see the effects of the execution. Or actually, the lack.

I believe that we should carefully acknowledge not only the point of view of our authorities but also their seating point. If we want to find out how to form a wall, do we ask an architect or a foreman? An architect may know the theory, but the practice is what we’re looking for. On the other hand, if you want to know where to put the wall, you prefer the architect to do measurements. At least if you don’t want to have the roof falling to your head.

After I had torn a ligament in my knee, I went to two qualified orthopedists. One said I should have surgery and do a reconstruction. The second stated that there is no need for that; rehabilitation should be enough. Guess which one had a specialization in surgery and which in rehabilitation?

People usually give us advice from the point where they’re currently standing. They are entitled to a biased view. An architect who rarely does programming will tend to downplay the value of implementation and tactical patterns. Midlevel developers will focus on technicalities instead of the global system impact. The team manager or consultant will emphasize the importance of soft skills (or esoteric techniques known only to them).

The truth is that we need all of them. The excellent plan will fall on the bad execution. The best execution for the wrong case will be just a waste of time. We should carefully evaluate the advice considering what we need and what an expert can give us.

Therefore, when we’re reading an article, watching a talk, let’s also pay attention to the place where the person is standing. The perspective from there may be much different from where we are right now. That can be good, as it may push us in the right direction. But it may also be misleading, as we accidentally take biases of this person without understanding the tradeoffs. Personally, I prefer to follow not only people from pedestal but also those that are closer to my position. A bit further in the journey, but not too far. That helps me to calibrate my view as those people are more relative to my daily struggles.

Polish historical leader Józef Piłsudzki reportedly used to say: “Right is like an ass, everyone has its own”.

Cheers!

Oskar