commanded / commanded

Use Commanded to build Elixir CQRS/ES applications

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Allow the aggregate's expected version to be included in command dispatch

slashdotdash opened this issue · comments

Add support for including the aggregate's expected version when dispatching a command.

If the aggregate's version:

  • equals the expected version then the command will be executed;
  • is greater than the expected version then the command will be rejected and an error returned (e.g. {:error, wrong_expected_version});
  • is less than the expected version then the aggregate will attempt to fetch any missing events and retry the command.

By default the expected_version option should be passed as :any_version to match the current behaviour.

Example

:ok = BankApp.dispatch(deposit_money, expected_version: 10)

is less than the expected version, then the aggregate will attempt to fetch any missing events and retry the command.

I am not sure if this would be something that we should do.

Scenario

Given a Bucket of money with $20, Person A wants to put $5 to fill up the bucket for the current week to be $25.

The Person A was looking at the screen the bucket said it had $20, so the person made the decision of putting $5 to fulfill the Person A demand.
While that was happening, Person B put $5 already; therefore, Person A put an extra $5 in the bucket.

Having a $25 is not an invariant; such a limit was decision-making among the peers who owned the bucket. Reasons why I would like to redirect the attention to made the decision based on what information we presented.

Concern

If such a scenario does not reject the Command as soon as there is a mismatch, you could successfully fetch any missing events and retry the command, and then it will put the extra $5 in the bucket.

As I said before, such a limit is not an invariant, so the consistent aggregate check will not help much. Such a decision was purely an Actor making a decision based on the information they had.


What would happen in that scenario?

The case where the aggregate's current version is less than the expected version is only really possible in a multi-node deployment where you have the same aggregate instance process running on different nodes and one of these receives the command but hasn't yet received the most recent event(s). Therefore it's state is stale due to eventual consistency of events being published to all nodes.

In this scenario it is acceptable to attempt to fetch any missing events and then do the expected version check again. This time the aggregate's version should be equal to the expected version (accept command) or could be greater than the expected version (reject command). If the aggregate version is still less than the expected version then the command can be rejected too.

From the Slack conversation,

The expected_version could be a tuple also:

[expected_version: {:gte, 20}]

Supported Filter

  • eq: equal to
  • gte: greater or equal than

@slashdotdash @yordis hello! I've been reading up on Elixir and Commanded and hope to use it for a startup soon, would love to help with closing out this issue!

For the :any_version default option, would it make more sense to leave that as a standalone default case? Trying to think about how to more cleanly capture both the filter suggestion (which I think is a great idea!) & the any_version default.

Thoughts on typing expected_version to be:
{:gte, integer} | {:eq, integer} | {:eq, :any_version} versus
{:gte, integer} | {:eq, integer} | :any_version?

{:gte, integer} | {:eq, integer} | :any_version

I would go for that, but either way, it would work.

I suggest making it work first, and then you can bikeshed over the final version.

Go with your gut feeling for now!

@yordis or @slashdotdash could I get contributor access to push up a pull request branch addressing the issue? Thanks!

@mksaga I will suggest forking the repository. Push the branch to your repository fork and then create a PR.

(Also, I do not have access to the Org at all, I am just another external contributor)