CQRS/Event Sourcing, как получить согласованные данные для применения бизнес-правил?

время от времени я разрабатываю небольшой проект с использованием шаблона CQRS и Event Sourcing. У меня есть структурная проблема, и я не знаю, какое решение решить ее.

Представьте следующий пример: Команда отправляется с информацией о том, что клиент банка заработал определенную сумму денег (DepositCommand). В обработчике команд /Entity/Aggregate (не важно для обсуждения) должно применяться бизнес-правило; Если клиент является одним из лучших 10%, при этом больше денег на счет выигрывает какой-то приз.

Вопрос заключается в том, как я могу получить последние, последовательные данные, чтобы узнать, находится ли клиент после его депозита в топ-10%.

  • Я не могу использовать хранилище событий, потому что невозможно сделать такой запрос;
  • Я не уверен, могу ли я использовать модель чтения, потому что не 100% убедитесь, что это актуально.

Как вы это делаете, когда вам нужны данные из базы данных для применения бизнес-правила? Если я не обращу внимание на современные данные, я сталкиваюсь с возможностями предоставить приз двум различным клиентам

С нетерпением ждем вашего мнения.

Ответ 1

Любая информация, которую агрегат требует для принятия бизнес-решений, должна храниться как часть совокупного состояния. Таким образом, когда получена команда для внесения денег на клиентскую учетную запись, у вас должно быть уже текущее состояние/обновление для этого клиента, который может содержать текущий баланс для каждой из своих учетных записей.

Я бы также предположил, что агрегат никогда не должен переходить к модели чтения, чтобы извлекать информацию. В зависимости от того, чего вы пытаетесь достичь, вы можете обогатить команду дополнительными деталями из модели чтения (где состояние не является критическим), но сам агрегат должен извлекать из него собственное известное состояние.


ИЗМЕНИТЬ

После повторного чтения вопроса я понимаю, что вы говорите о состоянии отслеживания в нескольких агрегатах. Это относится к царству саги. Вы можете создать сагу, которая отслеживает порог, который должен находиться в верхних 10%. Таким образом, всякий раз, когда клиент делает депозит, сага может отслеживать, где это размещает их в рейтинге. Если этот клиент пересекает потоки, вы можете опубликовать команду из саги, чтобы указать, что они соответствуют требуемым критериям.

В вашем случае ваша сага может отслеживать общую сумму всех депозитов, поэтому при внесении депозита можно принять решение о том, находится ли клиент сейчас в топ-10%. Другие вопросы, которые вы можете задать себе... если клиент внесет сумму в размере $X, и сразу же увеличит ширину $Y, чтобы вернуться обратно под thashashold; что должно произойти? Etc.


Очень грубые методы обработки агрегата/саги...

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?
}