Safe programming with idempotency
If you have never heard of the concept of idempotency, or you do not yet fully understand it, this article aims to help. We will explain by using an easy to understand, real world example.
You are at an elevator and press spamming the button to be sure it’s coming. It doesn’t matter how many times you press the button, the result is the same: the elevator is heading to your floor. In programming this “assurance” is called idempotency. This concept might sound complex, but in reality it can be quite simple.
Let’s give an example. Say you’re building a webshop and you want to send a payment confirmation e-mail when an order is marked as paid. Your payment service provider will send a webhook to your application whenever a payment has been made in real-time:
// request payload contains the id of the related order
// {
// "order_id": "ord_10008",
// "event": "order/paid"
// }
public function processWebhook(Request $request): Response
{
$orderId = $request->get('order_id');
$order = Order::find($orderId);
$order->status = 'paid';
$order->save();
Mail::to($order->email_address)->send(new OrderPaid($order));
return response()->ok();
}
This code seems to do what we want it to do:
- The order is fetched from the database
- The order status is set to paid
- An order paid e-mail is sent to the customer
- A success response is returned so the payment service provider knows the webhook is processed succesfully
Now here comes the catch. What happens when, for whatever reason, that “order/paid” webhook is sent twice? With above code, it will send a second OrderPaid
mail.
Currently, every time the application receives the webhook, it will trigger a new email. We certainly do not want that to happen. This scenario translates to the following diagram:
How can we ensure that this does not happen anymore? For this example, it’s actually pretty simple: we need to check if we previously handled this event:
public function processWebhook(Request $request): Response
{
$orderId = $request->get('order_id');
$order = Order::find($orderId);
if ($order->isPaid()) {
return response()->ok();
}
$order->status = 'paid';
$order->save();
Mail::to($order->email_address)->send(new OrderPaid($order));
return response()->ok();
}
We added a few lines of code which checks if the order has already been marked as paid. No matter how many times that same webhook will be triggered (erroneously or not) this check will make sure that the result is the same: just 1 order confirmation mail is sent. We don’t have to worry about receiving multiple webhooks anymore.
We now made this part of our application Idempotent. The diagram will look a bit different now:
In conclusion
Always try to ask yourself: what happens if this part of my code happens to be executed more than once? Can it do any wrong? If the answer is yes: ensure to make that part of the application idempotent.
Idempotency is not just relevant with projects usingmulti concurrency or asynchronous communication. What happens if a user spam-clicks a submit button and multiple requests are send, or someone refreshes a during a POST request?
P.S.
The example in this article can introduce a second problem: race conditions. Imagine when webhooks are being processed at the exact same time. Interested to learn how you can deal with this? I will be covering race conditions in my next blog post!