-
Notifications
You must be signed in to change notification settings - Fork 247
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
Conversation
There was a problem hiding this 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 |
There was a problem hiding this comment.
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 -> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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.
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.
{:ok, aggregate}
on success.{:error, reason}
on failure.The example bank account aggregate returns
{:error, :account_already_open}
should an attempt be made to open an existing account.The error will be returned to the caller when the command is dispatched.
Fixes #9.