Event Sourcing – How to Avoid Implementing Projections Twice

domain-driven-designevent-sourcing

I'm currently teaching myself event sourcing and have got the concept enough to start developing a dummy app in C# and EventStore. My app is an easy-to-understand bank account system.

If we model a bank account as a series of withdrawal and deposit events then we can easily calculate the account's current balance by adding up all the deposits and subtracting all the withdrawals, and EventStore gives us a way to implement this in the data store using projections.

However, if we have a business rule that dictates that accounts can never go overdrawn (i.e. we must reject requests to withdraw from an account if the current balance is insufficient to cover the requested funds), then our BankAccount entity also needs to know its current balance.

Again, this is easy to calculate by running over the series of bank account events but I have a deep unease with the fact that I have had to implement my projection of "balance" twice: once in the data store and once in the domain.

Is this expected from Event Sourcing, and I shouldn't worry about it? Or am I going down the wrong path, and in fact the balance should not be calculated in the domain code and should instead be retrieved from the data store every time it is required? This means that a round trip to the data store would be required every time an entity is modified.

I've seen in Greg Young's talks that it's supposed to be a natural fit for DDD so there shouldn't be an incompatibility there.

If I've misunderstood any part of this set up then any help would be greatly appreciated.

Best Answer

Your event log is your only source of truth. You cannot trust the data in the projections, because it is eventually consistent and does not have to be up-to-date. As you have mentioned, you can indeed get the latest state of your domain entity by reading all events related to that one specific entity and flatting them to your final representation.

So if an account can never be overdrawn, you need to know its balance, you need to calculate it in your domain object from the event stream. This is not only recommended but necessary.

Of course, if your projections are as simple as being the latest (eventually consistent) state representation of your entities, it might seem you are duplicating logic. However, the latest state of an entity is in most cases not the only representation of the entity, i.e. how you'd like to view it the data.

For different purposes you may (and with large systems are likely to) have different projections. Some can be entities, but other projections may be entire aggregations. With these aggregations you could then use an MoneyWasDeposited event from all BankAccount entities to update a monthly cashflow to see how much money is actually flowing through your system.

The strength of projections comes at the time when you find out you need different representations of - internally - the same data. Thanks to projection pre-building, increasing complexity of the view does not slow down load times for end users.


To followup on VoiceOfUnreason's answer, while e.g. sharing the logic for balance calculation makes sense in terms of DRY principle (so that you don't repeat yourself), when actually practicing ES, I have learned it mostly does not.

An event might look like this:

class MoneyWasDeposited: IEvent
{
    public BankAccountId ToBankAccountId;
    public Money Amount;
}

An event sourced aggregate then usually processed this event using a similar mechanism to this:

class BankAccount
{
    private BankAccountId id;
    private Money currentBalance;

    public void ReplayFromEvents(List<IEvent> events)
    {
        for (var event in Events)
        {
            ApplyEvent(event);
        }
    }

    private void ApplyEvent(IEvent event)
    {
        if (event is MoneyWasDeposited)
        {
            currentBalance = currentBalance.Add(event.Amount);
        }
    }
}

while your projection is actually likely to look something like this:

class BankAccountProjection
{
    private MySqlConnection database;

    public void HandleMoneyWasDeposited(MoneyWasDeposited event)
    {
        var statement = database.Prepare("
            UPDATE
                bank_accounts
            SET balance = balance + :depositedAmount
            WHERE id = :id
        ");

        statement.BindValue("depositedAmount", event.Amount.Value);
        statement.BindValue("id", event.ToBankAccountId.ToString());

        statement.ExecuteNoResult();
    }
}

So while your aggregate calculates the value in code from the event, the projection usually applies the change directly to your read-model store and let's the storage engine take care of the data modification.

As you can see, there's really no way to reduce duplication with this anymore, because the aggregate and projection use vastly different mechanisms to achieve the desired result.

Now, I am not saying you couldn't pull out the entire projection from the read-model store, update the read-model in memory and dump it back for update, but it's usually not the way how it's done, as it adds one unnecessary SELECT and achieves virtually the same result anyway.


You may ask: The balance calculation logic is not in a single place. So what if someone, by accident, makes the logic in the projection incorrect?

Consider the scenario that somebody wrote a minus sign to the projections' balance update when money was deposited. This would mean that every time you deposited money to your account, you would suddenly have less.

You would then do the following steps:

  • change the minus sign to a plus sign in the projection method,
  • push the change immediately to production, so that all changes from now on are ok,
  • create a new projection index in your read-model store, e.g. bank_accounts_fixed,
  • run another instance of bank account projection manager from the inception of your system and insert data into bank_accounts_fixed instead,
  • after the local projection finishes running, swap configuration on production to use the bank_accounts_fixed data in place of the bank_accounts_fixed,
  • delete the original bank_accounts index.

And all your user's will suddenly see correct balances. You can do this because you know that the data in your event store is guaranteed to be correct (it should be).

Related Topic