Question

at times I'm developing a small project using CQRS pattern and Event Sourcing. I have a structural issue and I'm not aware which solution to take to resolve it.

Imagine the following example: A command is sent with information that a client of the bank had deposit some amount of money (DepositCommand). In the command handler/Entity/Aggregate (is not important for the discussion) a business rule has to be applied; If the client is one of the top 10% with more money in the account win some prize.

The question is how can I get up to date, consistent, data to know if the client after its deposit is in the top 10%.

  • I can't use the event store because is not possible to make such a query;
  • I'm not sure if i can use the read model because is not 100% sure that is up to date.

How do you do, in cases where you need data from database to apply a business rule? If I don't pay attention to up to date data I run into possibilities of give the prize to two different clients

Looking forward to hear your opinion.

Was it helpful?

Solution

Any information that an aggregate requires to make business decisions should be stored as a part of the aggregate's state. As such, when a command is received to deposit money in to a client's account, you should already have the current/update state for that client which can contain the current balance for each of their accounts.

I would also suggest that an aggregate should never go to the read-model to pull information. Depending on what you are trying to achieve, you may enrich the command with additional details from the read model (where state is not critical), but the aggregate itself should be pulling from it's own known state.


EDIT

After re-reading the question, I realize you are talking about tracking state across multiple aggregates. This falls in the realm of a saga. You can create a saga that tracks the threshold required to be in the top 10%. Thus, whenever a client makes a deposit, the saga can track where this places them in the ranking. If that client crosses over the threadshold, you can then publish a command from the saga to indicate that they meet the criteria required.

In your case, your saga might track the total amount of all deposits so when a deposit is made, a decision can be made as to whether or not the client is now in the top 10%. Other questions you may want to ask yourself... if the client deposits $X amount of money, and immediately widthrawls $Y to drop back under the threashold; what should happen? Etc.


Very crude aggregate/saga handle methods...

public class Client : Aggregate
{
    public void Handle(DepositMoney command)
    {
        // What if the account is not known? Has insufficient funds? Is locked? etc...
        // Track the minimum amount of state required to make whatever choice is required.
        var account = State.Accounts[command.AccountId]; 

        // Balance here would reflect a point in time, and should not be directly persisted to the read model;
        // use an atomic update to increment the balance for the read-model in your denormalizer.
        Raise(new MoneyDeposited { Amount = command.Amount, Balance = account.Balance + command.Amount }); 
    }

    public void Handle(ElevateClientStatus command)
    {
        // you are now a VIP... raise event to update state accordingly...
    }
}

public class TopClientSaga : Saga
{
    public void Handle(MoneyDeposited e)
    {
        // Increment the total deposits... sagas need to be thread-safe (i.e., locked while state is changing).
        State.TotalDeposits += e.Amount;

        //TODO: Check if client is already a VIP; if yes, nothing needs to happen...

        // Depositing money itself changes the 10% threshold; what happens to clients that are no longer in the top 10%?
        if (e.Balance > State.TotalDeposits * 0.10)
        {
            // you are a top 10% client... publish some command to do whatever needs to be done.
            Publish(new ElevateClientStatus { ClientId = e.ClientId, ... });
        }
    }

    // handle withdrawls, money tranfers etc?
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top