Guaranteeing exact-once execution in unreliable networks to prevent disastrous double-charges.
"An operation is idempotent if it produces the same result regardless of whether it is executed once or multiple times."
execute same operation multiple times, result is same as if operation was applied justONCE
No matter how many times user likes one post, the like count should just increase by 1
Say A wants to transfer $ 20,000 to B, and if due to any reason the API call is retried.
We would not want A to transfer twice the amount to B.
You retry when something goes wrong
Deducted
From ACredited
to B
Retry only when
you are sure
Client knows that the request didn't even reach the server, hence, safe to retry
Failure while server is executing. Client cannot be sure of retrying.
Server completed execution but before the response reach client, network fails. Client cannot sure of retrying.
Depending on your product/usecase this might be the best thing
If operation failed, propagate the error and show it to the end user
End user retries if he/she wants to
Idea:Get the status of payment & process only when not already processed.
create a unique payment_id
and weave your API calls with it
Your API server can use this to get, check & update if needed
Payments service talks to Payment Gateway & generates a
Payment ID
Payment service initiates the payment through this ID
If payment service retries, it first
checks the status of payment on ID
if the status is COMPLETED
then does not retry
else retry through the same ID
Here is a conceptual implementation of an idempotent API using Express.js. It demonstrates the "Check and Update" logic by validating a unique Idempotency-Key header against an internal data store.
import express from "express";
const app = express();
app.use(express.json());
// Mock Database table for idempotent requests
// Schema: { payment_id: string, status: "PROCESSING" | "COMPLETED", result: any }
const idempotencyStore = new Map();
app.post("/api/pay", async (req, res) => {
const { amount, from_user, to_user } = req.body;
const payment_id = req.headers["idempotency-key"];
if (!payment_id) {
return res.status(400).json({ error: "Idempotency-Key header is required" });
}
// 1. Check if the payment_id has been seen before (Generate ID / Check Status)
if (idempotencyStore.has(payment_id)) {
const record = idempotencyStore.get(payment_id);
if (record.status === "COMPLETED") {
// Return exactly what was returned the first time (Already Completed)
return res.status(200).json({
message: "Already completed.",
data: record.result
});
}
if (record.status === "PROCESSING") {
// Concurrent request issue (e.g. user double clicked quickly)
return res.status(409).json({ error: "Request is already being processed." });
}
}
// 2. Mark as processing to handle concurrent identical requests
idempotencyStore.set(payment_id, { status: "PROCESSING", result: null });
try {
// 3. Process the actual payment logic (A -> B, $20,000)
const result = await processPayment(from_user, to_user, amount);
// 4. Update the record status to COMPLETED
idempotencyStore.set(payment_id, { status: "COMPLETED", result: result });
return res.status(200).json({ message: "Transfer done", data: result });
} catch (error) {
// If it fails, remove the key so it can be safely retried
idempotencyStore.delete(payment_id);
return res.status(500).json({ error: "Internal processing error." });
}
});