Idempotency is a concept well known in the financial sector, but not much outside of it. In fact, I never heard of it before starting my job at an online payments company. Nevertheless, idempotency can be very benifcial to other sectors too. In this post, I’ll introduce the idea behind idempotent APIs and how to design them.
What is Idempotency
Idempotency, simply put, means that repeating the same request to an API multiple times will yield the same outcome as if done once. For example, if we sent a request to create A, then sent the same request again, we should still have only one A.
Why Idempotency?
Nothing is guaranteed to be up 100% 24/7 forever; API calls fail for all kinds of reasons. However, the failure of an API call doesn’t necessarily mean that the request wasn’t executed. The response might have failed to reach the caller even though the action was carried out.
In another scenario (shown in the figure below) more focuesed on microservices, you might have a single incoming request from outside. That service at the front might need to call other services to complete the request and each of those services will carry out certain actions. Now, what if one of those failed but the ones before it succeeded? An error response will be returned to the client and the client might attempt the same call again. If those services had idempotent APIs, that wouldn’t matter. Of course, in that scenario, sagas are also a viable solution.
You can definitely see the appeal of designing idempotent APIs when it comes to any payments service. You don’t want to charge someone twice, or credit their account three times because of intermittent network failures. But it can have other applications outside the financial sector too, like avoiding sending multiple emails for the same notification if you have your own notifications service..etc.
How to Make an API Idempotent
We explained the “what” and the “why”, now let’s dive into the “how”. Making an idempotent API is actually simple and will, pretty much always, follow the following steps:
- The server can expect a value (we refer to it as idempotent key) from the client which identifies the request. This value could be provided as an HTTP header in the case of REST APIs for example.
- The server checks if another request was executed with the same value.
- If a previous successful request had that value, then the call fails and no other actions are taken. Otherwise, the server will process the request then create a record for it.
That’s the general idea, and as you could see it’s both simple and easy to implement.
Now let’s go over some certain scenarios and tips for having an effective implementation.
Multiple Changes with the Same Key
Since the same idempotent key could be used to trigger multiple distinct actions, we can’t just rely on checking if a record with that key exists. We need a way to identify that a record of particular action with that key exists. One way to do this is to have the record have the following structure:
{
"id": String,
"idempotentKey": String,
"entityType": String,
"entityId": String
}
We can then search using the idempotent key and the entity type before applying a change. This is quite simplistic and works for most cases, however you might need to add other information about it.
Storing the Response
In the case of the previous structure, we have the key, the entity type which was affected, and the ID of that entity. In some scenarios, we might want to return the exact response which was sent to the client before. In which case you might opt for storing the full response as well.
Deterministic Keys
More often than not, the idempotent will be randomly generated by the client, and will be the same for few retries before the client gives up and aborts the whole operation. In other cases, however, we might prefer having a deterministic way to derive the key from something else.
Let’s say you have an event listener which will perform some operations based on some events. In this case, your key might be the ID of the message itself. This is particularly very useful since exactly-once processing is extermely hard in event processing. This is also quite useful if you have a retry policy in place which would retry an event in case of failure later, in few minutes or even hours. You’re guaranteed to always have the same key.
Conclusion
As you can see, idempotent APIs are quite useful, and idempotency is definitely a good option to consider in the future for your APIs. This was the general idea with few points to highlight certain cases, you might end up running into some scenarios where a different approach might be taken instead.