1

I know that the general concept behind event sourcing is that the state of the application should be able to be replayed from the event stream.

Sometimes, however, we need to get information for business rules from other parts of the system. i.e. An account has a user. A user has a blacklist status which is required to check if they can access/edit the account.

In the below example (purely for demonstration purposes), a user tries to subtract $10 from their account. If a user has been blacklisted, then we do not want to allow them to remove any funds from the account but we do want to record that they have tried to.

After the request is made, we could query the user model to see if the blacklist exists. If true then we can record it and throw the exception.

The user table/model is currently not event-sourced.

Now when we try to replay the event stream to re-build the projections with the state of the user is not being stored in events, it is no longer possible.

So assuming my current example does not work my questions are:

  1. If we were to move the user into an event stored system (in a different aggregate but all events within the same event-stream) then would it be acceptable to use read models within business rules?

  2. Is there any way we can mix event-sourced and CRUD into the same system when they may depend on each other for business rules.

public function subtractMoney(int $amount)
{
    if ($this->accountOwnerIsBlacklisted()){
        $this->recordThat(new UserActionBlocked());

        throw CouldNotSubtractMoney::ownerBlocked();
    }

    if (!$this->hasSufficientFundsToSubtractAmount($amount)) {
        $this->recordThat(new AccountLimitHit());

        if ($this->needsMoreMoney()) {
            $this->recordThat(new MoreMoneyNeeded());
        }

        $this->persist();

        throw CouldNotSubtractMoney::notEnoughFunds($amount);
    }

    $this->recordThat(new MoneySubtracted($amount));
}

private function accountOwnerIsBlacklisted(): bool
{
    return $this->accountRepositry()->ownerUser()->isBlackListed();
}

2 Answers 2

1

First, let me consider this:

Now when we try to replay the event stream to re-build the projections with the state of the user is not being stored in events, it is no longer possible.

I don't see why it would be no longer possible. After all, the event stream does contain a UserActionBlocked event (judging from your code), so you would just replay that and know that the action did not take place. The knowledge of the blacklist is not required for replaying.

In case I misunderstood what you meant by that, could you please clarify?

Now, to answer the questions:

If we were to move the user into an event stored system ... would it be acceptable to use read models within business rules?

No, this is generally a no-no. The reason you can't use read models for handling commands is that read models are eventually consistent. This means that at the moment you're handling a command the read model may not yet represent the true "current" state of the system, so you'd be basing your decisions on outdated information.

Is there any way we can mix event-sourced and CRUD into the same system when they may depend on each other for business rules.

As described above, commands may consult non-event-sourced data just fine, as long as they record the results of such considerations in the events. If the event says "user action has been blocked, because the user is blacklisted", we know that at the moment the event was issued, the user was blacklisted. If we're looking at the event in the future, we may no longer have access to that past blacklist, but we still know what it was from the event.

1

I know that the general concept behind event sourcing is that the state of the application should be able to be replayed from the event stream.

Sometimes, however, we need to get information for business rules from other parts of the system.

Short answer: Obtaining a stale copy of somebody else's data can be an event, which is to say, you can treat that information as though it is an input into your system, and handle it accordingly.

Logically, you go from something like this:

newEvents = domain.model(command, eventHistory)

to something like

newEvents = domain.model(command, eventHistory, dataCopiedFromTheOtherService)

The domain model does the calculations, the application layer (aka the command handler) solves the problem of acquiring copies of the required information. In complicated examples, you end up with a sort of protocol, with the command handler orchestrating interactions between the domain model, the repository, api clients, and local caches.

Yes, you can absolutely use read models as a mechanism for copying stale information from one service to another. You will want to be careful about the fact that the data is stale (the authoritative service can change the "real" copy after answering the query), and that the other service may not be available (ex: the other service is down for maintenance).

Another concern: make sure that you are creating the correct service boundaries - in most cases, you want the business capability to be in the same place as the data. If there is a piece of data that you are regularly using "somewhere else", then that's a hint that it is in the wrong spot.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.