Aggregate-roots are there to control the state-changes - what is allowed currently and what's not. If the state-transition is allowed, go on. If not, you throw an exception explaining the reason why it was not allowed.
But what is if a state-change doesn't happen because it's already in the requested state?
For example, if you have a Approve
method on your aggregate-root and by the time it is called the state is already approved?
- Should an exception be thrown a la "XYZ is already approved"?
- Or should it be silently ignored?
- Or should the state-change be "signaled" again (event-sourcing, next paragraph)?
In my case I use event sourcing, so an event is being emitted if a state-change occured. Having events in my event-stream with no real state-change doesn't feel "clean" to me because I'd like to be confident that the events actually were produced due to state-changing actions.
Is there a rule of thumb?
EDIT:
In the described case approving an approved element would not really hurt. So tend to this way (thank you @Eben Roux, @guillaume31).
But let's add a bit more spice to it (the actual question behind the question):
Assume:
- message-bus
- async command/event handling
- a process-manager
What if a process-manager (aka saga) issues a command (async) and want to know if a command succeeded? I think it would reduce the mental load/error-sources if the process-manager would not have to care about this implementation detail.
I see 3 ways of handling that:
- have a "Command with id ABC succeeded/failed" message sent on bus
process-manager waits for that message instead of the event - make command execution sync
if process-manager encounters no exception, everything's fine, move on - introduce a new event
ApprovalDeclined { WasAlreadyApproved = true }
additionally the process-manager waits for this event - the declination is part of the aggregate history, maybe an advantage, maybe never needed...
I know: "it depends"
But can you think of any other (more elegant/easier/different) solution? What's your favorite "process-manager-compatible" way of dealing with this issue?