Idempotent is a funny word when used outside of the context of development. In fact, it’s a term that comes from
mathematics. It describes an element that can be operated on more than once and produces the same value every time. For
example, the operation, 1 * 1
, in traditional multiplication will always produce 1
.
Within the world of development, the definition is almost the same.
For a microservice to be idempotent it must produce the same output if given the same inputs. Or in other words, it is idempotent if it’s logic can be invoked more than once and produce only one side effect.
Let’s explore this idea of one side effect per unique invocation.
One side effect per unique call
Take our 1 * 1
example from up above, our service might take two inputs n1
and n2
and return n1 * n2
.
function runMultiply(n1, n2) {
return n1 * n2
}
The side effect here is the multiplication operation, the logic of our service. In its current state, this side effect
happens every time runMultiply
gets called. Even if our inputs remain the same, n1 = 1 and n2 = 1
, we still run our
operation n1 * n2
and return the value 1
. Our service is idempotent, but is it producing one side effect per unique
call?
By our most recent definition, no it’s not. For a simple multiplication service like this one, it’s not the end of the world. It’s not creating any new resources, updating existing ones, or performing a complex algorithm. So running the logic again for a duplicated request isn’t going to hurt anything.
Does idempotency imply exactly-once semantics? No.
As we saw with our multiplication example, we can have an idempotent service that does not have exactly-once semantics.
This tends to happen when the service is stateless. They take some inputs and performs a stateless operation as we saw
with our runMultiply
.
Let’s take a look at an example that illuminates the need for exactly-once semantics.
Say our microservice was responsible for creating a Person
record in our database. The inputs to our service might be
a name, address, and phone number. The first time our service gets invoked, we are going to create a new Person
record
for this name, address, and phone number combo.
function createPerson(name, address, phoneNumber) {
const person = {name: name, address: address, phoneNumber: phoneNumber}
// insert person into database
return person
}
If we did nothing else to this service, is it idempotent?
It’s not. A second invocation with the exact same inputs (name, address, phone number) would cause another new
Person
to get created. This service will create duplicate people if invoked more than once with the same inputs.
The fix to make it idempotent is to check if there is already a Person
with these values. If so, don’t create a new
one and return the person that is already present. If there isn’t an existing object, create the new one and return
success.
This is guaranteeing that exactly one side effect will happen. In other words, exactly one person will be created for each unique combo of name, address, and phone number.
Benefits of Idempotent
So what does idempotent unlock for us that we would otherwise be missing out on? There are a lot of benefits realized when our services are idempotent. But let’s focus on three key ones.
-
Resilient: We have the ability to replay any invocations with the guarantee that a duplicate invocation won’t produce a new side effect. This is huge in all types of systems because it means that if an error occurs we can simply retry the invocation.
-
Parallel Processing: An often overlooked benefit, we can process multiple invocations in parallel. If one of them fails, that’s OK because we’re resilient and can retry it.
-
Data Consistency: By guaranteeing exactly one side effect per unique invocation we can guarantee that our data is consistent. Meaning we don’t have duplicate records or malformed data from incomplete invocations.
Resiliency is the key benefit to hone in on here. By being able to retry failed invocations we have unlocked a new world of scalability and performance. In distributed systems where dozens of microservices are interacting with one another having resiliency baked in is a huge win.
Conclusion
Idempotent sounds complex and a bit cumbersome. Its implementation can be complicated but the core idea is sound. Build services that can handle duplicate invocations while still only producing one side effect. If we keep the definition in mind as our guiding light we can refer to it as we are implementing a new service or updating an existing one.
Exactly-once semantics allows us to build systems that are resilient to failures. This is because a single invocation acts as a transaction between the service and the client. If this invocation is new, the service will execute its full logic. If this invocation is not new, the service will still succeed but not produce a second side effect.