DEV Community

Cover image for Idempotency - What it is and How to Implement it
Alex Hyett
Alex Hyett

Posted on • Originally published at alexhyett.com

Idempotency - What it is and How to Implement it

When designing an API it is easy to think about the happy path of our applications but if you want to build a robust application and keep your users happy you need to think about the unhappy paths as well.

What happens if the user's internet cuts out just after making a request or your server is struggling under load and the client times out before they can receive the response?

Usually, errors are handled by the client by doing retries. If there is an error or the request times out the client will retry a number of times either automatically or by frustrated users smashing the submit button.

If the user sends multiple requests to the server and more than one of them gets through you may end up with duplicates of things on your end.

Now depending on the application this might not be a big deal. If the user is just writing a comment on a blog post or reviewing a product, what if they end up with multiple posts it is not the end of the world.

But what if they were sending an email, is it acceptable for the recipients to receive multiple copies?

What if they were buying something online? Would the customer be happy if you charged them twice or even ten times for the same product?

This is where idempotency comes in. But what does it actually mean?

What is idempotency?

If we have a look at the Internet Standards, RFC 9110 says the following:

A request method is considered "idempotent" if the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request.

So if we call an API multiple times with the same request, then it is considered idempotent if it has the same effect on the server as if we had only sent one request.

Now let's have a look at the most common HTTP methods and see if they are idempotent or not.

GET

This is a read-only method, it doesn't affect the state of the server in any way. Nothing is being created or updated here so we can call a GET method multiple times without any adverse effects. So a GET method is always idempotent.

PUT

With a PUT request, we are performing an update. Let's say we are updating a user's email address.

PUT /users/3848

{
    "first_name": "John",
    "last_name": "Smith",
    "email": "john.smith@example.com"
}
Enter fullscreen mode Exit fullscreen mode

No matter how many times we call this method we should always get the same response and his details will always be updated to the same thing.

So PUT requests are also always Idempotent.

POST

With a POST request, we are creating something on the server. The expectation is if we call a POST request multiple times we are either going to get multiple copies of the same thing or in some cases, we might get an error as the object with that name already exists.

For example, if we are trying to create a user using a POST endpoint, the first request will go through but the second request will fail.

Technically this is still idempotent but it isn't a very nice experience for the user if they didn't get the response to their first request. They may think the username is already taken even though it was their first request that created it.

If we take the example of making a payment. Generally, there aren't any unique identifiers on a payment like there are with creating a user.

If we look at the following request:

POST /payment

{
    "amount": 2000,
    "currency": "usd",
    "card_number": "4122916040150031",
    "cvv": "407",
    "expiry_year": "2025",
    "expiry_month": "07" 
}
Enter fullscreen mode Exit fullscreen mode

This is what would get sent through on a payment platform such as Stripe if someone was buying something for $20.

However, there is nothing unique about this request that links it to a particular purchase. If the payments platform was to receive 2 of these in a row it would just assume these are 2 separate payments. Which is not what we want.

So POST requests are not idempotent.

PATCH

With patch requests, it depends on how you use them. If you are using them to simply update the email address of a user without having to send other details through you could consider this idempotent.

PATCH /users

[
    { "op": "replace", "path": "/users/3848/email", "value": "john.smith@example.com" }
]
Enter fullscreen mode Exit fullscreen mode

However, you can also use a PATCH request to perform other operations such as copy, move, add and remove so it isn't considered idempotent.

DELETE

Finally, we have the delete method which can be used to delete an object on the server.

Delete can be run multiple times and it will always have the same final effect on the server as if it was only performed once.

If you try to delete something twice the second time round you will likely get an error but the state of the server doesn't change. Therefore DELETE is idempotent.

Of course, this only applies if you are using DELETE correctly and deleting a whole object. If you are using DELETE to change the quantity of something then this is just wrong and you should be using either POST, PUT or PATCH if you want to test the value first:

[
    { "op": "test", "path": "/Quantity", "value": 2 },
    { "op": "replace", "path": "/Quantity", "value": 1 }
]
Enter fullscreen mode Exit fullscreen mode

Implementing Idempotency

In the cases where you have a payment or another critical operation that you need to happen exactly once you need to make those non-idempotent methods such as POST and PATCH idempotent as well.

The only way you can do this is to ensure that your request has some form of unique identifier that you can use to distinguish it from otherwise identical requests.

You may have this in place already. Going back to the payments analogy, for example, a saved basket of products could have a unique ID associated with it that could then be used for idempotency.

Another option is to hash the body of the request and use that as your unique identifier. However, you need to be sure if you are doing this that there is no reason the person would want to send exactly the same request again. There should be something about the combination of fields that makes it unique.

If someone orders a SpongeBob beach towel and then 30 seconds later orders another SpongeBob beach towel you don't know that this is a duplicate order, they might just really like SpongeBob.

Generally, the way this is normally done is to add a header that contains an idempotency key. Stripe for example uses the header Idempotency-Key but really this could be anything.

So you now have some form of key that makes this request unique.

With a user-supplied idempotency key, you can't guarantee that what they send you is going to be globally unique. They could end up giving you the same idempotency key as another user or the same key for multiple endpoints.

So it is important that you combine the idempotency key with a user ID and the API path that has been used.

Idempotency Storage

To implement idempotency you need to store your combined idempotency key somewhere along with the successful response that you send the user.

If a request with the same idempotency key comes in, instead of doing the operation you simply return the response that you sent out before. As far as the user is concerned the operation was successful and they don't need to know that you are sending them the previous response.

Typically this is done using some form of Key Value storage such as Redis or Dynamo DB.

Technically you could store the idempotency keys in an in-memory key value storage such as a dictionary. However, this won't work if you have multiple copies of your API running behind a load balancer, as each API wouldn't know about the keys stored in the other application. You will also lose those keys every time you restart your application. This is why we generally store these separately from our applications.

When a request comes in you check to see if there is a match for that key in the storage. If there is then you return the response that you stored.

If there isn't then you carry on with the normal operation and then store the response before sending it back to the user.

How long you store your idempotency keys and responses really depends on your application. If it is just to prevent the user from sending a request twice due to a double click then 10 minutes or so might be ok. Generally, from what I have seen people store idempotency keys for around 24-48 hours.

However, if you have an event-based system where you might want to replay events that happened months ago then obviously your idempotency keys will need to last longer.

If you are using Redis or DynamoDB you can set the Time To Live or TTL on the items so they are deleted automatically for you.

Idempotency Validation

If a request comes in from the same user, for the same endpoint with the same idempotency key as a previous request we are assuming it is just a duplicate.

However, there is a chance that the user has used the same idempotency key by mistake and the body of the request is actually very different.

If you need to protect against this scenario then you can take a hash of the request body and then store that alongside the idempotency key and response and return an error if the request is different.

Implementation in .NET Core

If you want to implement this in a .NET Core API then you can make use of the Microsoft Distributed Caching library to store your keys in Redis.

You can then set up an idempotency filter that you add to particular endpoints to handle the storage of the requests and responses.

To make this easier there is Nuget Package called IdempotentAPI that you can use that will do all the heavy lifting for you.

If you want to support this channel and also get access to the code shown in this video then you can subscribe to my Patreon.

Patreon members also get access to exclusive content from me, a private Discord community and will generous discounts on my courses when they come out.


πŸ“¨ Are you looking to level up your skills in the tech industry?

My weekly newsletter is written for engineers like you, providing you with the tools you need to excel in your career. Join here for free β†’

Top comments (1)

Collapse
 
ant_f_dev profile image
Anthony Fung

Thanks for the insight into Stripe transactions.

On a related note, idempotency can be used to describe pure functions too. Here the outcome is solely dependent on transforming its inputs in the same way, so you'll always get the same output when calling the function with the same inputs.