(This is part 2 of a long-running series on idempotence. You can go to the beginning.)
With state-based idempotence business rules of your system determine if a certain operation has already been carried out.
Thanks, folks. See you tomorrow!
Just kidding. We can see an example of this in The Eventide Project’s account-component
example—specifically the opening lines of code in the Open
handler:
handle Open do |open|
account_id = open.account_id
account, version = store.fetch(account_id, include: :version)
if account.open?
logger.info(tag: :ignored) { "<Helpful log message"> }
return
end
# ...
end
The line account, version = ...
runs what we call a projection. In an event-sourced system, projections define how to turn append-only logs of immutable events into the current state of some entity, in this case a bank account.
In the if
expression, we then ask account
if it’s already open. When the account is already open, we exit the handler early.
Why would an account already be open?
I can imagine a couple of reasons. First, you may have a bug in your system that allows users to declare the intent to open an account a second time.
Second, networks fail.
Whether you have HTTP, Eventide with Message DB, Kafka, or whatever, networks fail. In a distributed system, you will see duplicated requests. Think of it like gravity, but for distributed systems.
When do we want to use state-based idempotence?
With the classic “it depends” caveat, if we can use state-based idempotence, we want to. It protects us against reprocessing the same message, and new messages declaring the same intent.
Performance concerns sometimes prevent us from leveraging state-based idempotence, so tomorrow we’ll go through another strategy called sequential idempotence.