Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't persist an aggregate's pending events when executing a command returns an error #10

Merged
merged 4 commits into from
Sep 29, 2016

Conversation

slashdotdash
Copy link
Member

@slashdotdash slashdotdash commented Sep 26, 2016

Don't persist an aggregate's pending events when executing a command returns an error.

An Aggregate should enforce domain rules by returning an error tuple instead of its state on failure.

  • An aggregate function must return {:ok, aggregate} on success.
  • An aggregate function should return {:error, reason} on failure.

The example bank account aggregate returns {:error, :account_already_open} should an attempt be made to open an existing account.

defmodule Commanded.ExampleDomain.BankAccount do
  def open_account(%BankAccount{state: %{is_active?: true}} = account, %OpenAccount{}) do
    {:error, :account_already_open}
  end

  def open_account(%BankAccount{state: %{is_active?: false}} = account, %OpenAccount{account_number: account_number, initial_balance: initial_balance}) when is_number(initial_balance) and initial_balance > 0 do
    account = 
      account
      |> update(%BankAccountOpened{account_number: account_number, initial_balance: initial_balance})

    {:ok, account}
  end
end

The error will be returned to the caller when the command is dispatched.

# open account should initially succeed
:ok = Router.dispatch(%OpenAccount{account_number: "acc1"})

# attempting to open same account should error
{:error, :account_already_open} = Router.dispatch(%OpenAccount{account_number: "acc1"})

Fixes #9.

Copy link
Contributor

@andrzejsliwa andrzejsliwa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, reviewed and added some comments about my personal preferences for handling errors more strictly (to let it fail on process level vs defensive style)

else
{:error, :wrong_expected_version} -> retry_command(command, handler, state, retries - 1)
{:error, _reason} = reply -> {reply, state}
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good!

:ok ->
# clear pending events after successful store
{:ok, %{aggregate_state | pending_events: []}}
{:error, :wrong_expected_version} = reply ->
Copy link
Contributor

@andrzejsliwa andrzejsliwa Sep 27, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could have here strong matching:

{:ok, state} = EventStore.append_to_stream(...)

to fail immediately, otherwise with will suppress error and only logger will be the evidence of problem. I personally prefer matching result for success case only and fail in place where errors occurs. In this case this will stop the process with information about state and reason. This is all about using OTP to have less defensive code and handle only happy cases.

Defensive code with with make sense for me in places where you need to handle user input/output or in business logic, but even then I'm always thinking if handling of not success case can be handled on level of supervision tree instead of defensive code.

Copy link
Member Author

@slashdotdash slashdotdash Sep 27, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you suggesting to remove the {:error, :wrong_expected_version} match and only match on :ok. This would crash the aggregate process if persisting the pending events fails due to version mismatch. That would follow the 'let it crash' idiom.

I haven't yet thought too much about retrying command handling on failure. I'd like this to be client configurable (e.g. 5 retries, with exponential back-off). Using GenRetry is one approach I am considering.

Copy link
Contributor

@andrzejsliwa andrzejsliwa Sep 27, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you suggesting to remove the {:error, :wrong_expected_version} match and only match on :ok. This would crash the aggregate process if persisting the pending events fails due to version mismatch. That would follow the 'let it crash' idiom.

yes, exactly. GenRetry is good example, can wrap in function this call
{:ok, state} = EventStore.append_to_stream(...) ...
or whole call of persist_events/2

{:error, reason} = reply ->
Logger.debug(fn -> "failed to handle command due to: #{inspect reason}" end)
reply
aggregate_state -> {:ok, aggregate_state}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think about returning {:ok, state} | {:error, reason} also from handler?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good suggestion. Being explicit about handlers returning :ok and the aggregate state on success makes sense. I'll look at making this change.

@slashdotdash slashdotdash changed the title Handle aggregate error Don't persist an aggregate's pending events when executing a command returns an error Sep 27, 2016
Let the aggregate process crash instead. Starting a new process for the
aggregate will load its events to restore it to a valid state.
…return `{:ok, aggregate}` or `{:error, reason}`

To allow strict pattern matching on success/failure.
@slashdotdash slashdotdash merged commit c781128 into master Sep 29, 2016
@slashdotdash slashdotdash deleted the feature/handle-aggregate-error branch September 29, 2016 21:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants