(This is part 2 of a long-running series on idempotence. You can go to the beginning.)
Yesterday we examined state-based idempotence, which protects against processing a particular domain operation more than once, even if you get additional messages telling you to do so.
Sometimes state-based idempotence doesn’t work. For example, again referencing the Eventide account-component
example, how would an account know if it has processed a given Deposit
request? The amount? No, you can deposit $5 twice into the same account. The time of the depoist would rely heavily on clock synchronization and low transaction volume.
We could give each deposit a unique identifier that the account could track, but imagine a long-running account with millions of transactions. Scanning that list each time to see if we’ve processed a deposit would be time and memory intense.
Sequential idempotence leverages a message’s global position
Every message in Message DB has a global_position
property, or its position in the sequence of messages throughout the Message Store. Commands are messages and precede any events they cause. It follows then that any event produced by a command would have a global_position
strictly greater than the command that caused it.
When we write an event from a command, we can store the command’s global_position
on the event as a sequence
property (see an example in account-component
).
When we project the account
, we can copy any sequence
found in an event to the account
.
So then, given a command, we could ask the account
component, “have you already processed through this command’s global_position
?”, which is exactly what account-component
does with these lines in the same handler linked above:
# ...
sequence = withdraw.metadata.global_position
if account.processed?(sequence)
# ...
return
end
And boom. This handler will never process that same message more than once.
Caveats
This doesn’t mean we should always use this mechanical idempotence method. First of all, it depends entirely upon message global_positions
, which in turn depend on which Message Store contains those messages. Sometimes, for operational reasons, we move categories of messages to different Message Stores. All of your sequence
s would be shot to heck after such a move.
Another consideration, while sequential idempotence protects against processing the same message more than once, what happens if you have two separate messages that are trying to do the same thing? What if your clients send two separate messages for deposit 123 because, wait for it, networks fail, and they didn’t know if the message was received?
In this case, we need a way to recognize that all these messages have the same intent. That’s what The Reservation Pattern solves, and it’s what we’ll cover tomorrow.