Idempotency artinya request yang sama boleh dikirim sekali atau berkali-kali, dan hasil akhirnya di server tetap sama. Bayangin bayar di kasir, lalu sinyal QRIS putus pas loading. Kamu gak tahu uangnya sudah terpotong atau belum. Kasir yang baik sudah menulis nomor nota sebelum kamu bayar. Waktu kamu balik dan bilang "nota 0457, tadi jadi gak?", dia mencocokkan nomornya dan menyerahkan struk yang sama, bukan menagih lagi. Nomor nota itu Idempotency-Key, dan struk yang sama itu response yang disimpan server.
🧪 Tebakan cepat: pembeli menekan Beli untuk satu Laskar Pelangi. Server membuat pesanan 501, tapi jawabannya hilang di jalan. Aplikasinya menunggu, lalu mengirim request yang sama persis sekali lagi. Ada berapa pesanan di server? Tebak dulu, jawabannya ada di bagian "Retry setelah timeout".
Method yang idempoten dari sananya
Di pelajaran HTTP Method kamu sudah kenal "aman dan idempoten". Menurut standar HTTP:
| Method | Idempoten? | Kenapa |
|---|---|---|
GET, HEAD |
ya | cuma membaca |
PUT |
ya | mengganti seluruh isi: dua kali tetap sama |
DELETE |
ya | yang sudah terhapus tetap terhapus (balasan kedua boleh 404) |
POST |
tidak | tiap kiriman membuat data baru |
PATCH |
tergantung isinya | lihat contoh di bawah |
PATCH idempoten kalau mengisi nilai mutlak, dan tidak kalau menambah atau mengurangi:
PATCH /books/1body{"stock": 11}terkirim 3 kali: stok tetap 11.PATCH /books/1body{"stock_delta": -1}("kurangi satu") terkirim 3 kali: stok 12 jadi 9. Perintah tambah-kurang seperti+1itu gak idempoten. Ubah jadi nilai mutlak, atau lindungi dengan Idempotency-Key.
Retry setelah timeout
Client yang gak dapat jawaban gak bisa membedakan "request-nya gak sampai" dengan "request-nya sampai, jawabannya yang hilang". Pilihan aman baginya cuma satu: kirim lagi. Untuk POST /orders, itu berbahaya:
| Waktu | Kejadian | Pesanan | Stok Laskar Pelangi |
|---|---|---|---|
| 10:20:00 | POST /orders body {"book_id": 1, "qty": 1} |
501 | 11 |
| 10:20:00 | server menjawab 201, jawabannya hilang |
501 | 11 |
| 10:20:10 | client menyerah setelah 10 detik, lalu retry | 501, 502 | 10 |
Dua pesanan, saldo terpotong Rp178.000 untuk satu buku. Itu jawaban tebakannya. Server gak salah: dari sisinya, request kedua memang pesanan baru.
Idempotency-Key: nomor nota dari client
Client membuat satu key unik sekali per aksi (satu klik Beli), lalu mengirimnya di header Idempotency-Key. Retry memakai key yang sama.
const key = crypto.randomUUID(); // sekali, saat tombol Beli ditekan
function createOrder() {
return fetch("https://api.tokobuku.test/orders", {
method: "POST",
headers: { "Content-Type": "application/json", "Idempotency-Key": key },
body: JSON.stringify({ book_id: 1, qty: 1 }),
});
}
Server mencatat tiap key bersama sidik body-nya dan response-nya:
const saved = new Map(); // key -> { fingerprint, status, body }
app.post("/orders", (req, res) => {
const key = req.headers["idempotency-key"];
const fingerprint = JSON.stringify(req.body);
const hit = key ? saved.get(key) : undefined;
if (hit) {
if (hit.fingerprint !== fingerprint) {
return res.status(422).json({ error: "Idempotency-Key reused with a different body" });
}
res.set("Idempotent-Replayed", "true");
return res.status(hit.status).json(hit.body); // jawaban yang disimpan, apa adanya
}
const order = { id: nextId++, book_id: 1, qty: req.body.qty, total: 89000 * req.body.qty };
stock -= req.body.qty;
if (key) saved.set(key, { fingerprint, status: 201, body: order });
res.status(201).json(order);
});
req.headers["idempotency-key"]: Express menyimpan nama header dalam huruf kecil.hitada: key ini pernah diproses. Server memutar ulang response yang disimpan:201dengan pesanan 501 yang sama, plus headerIdempotent-Replayed: true(Stripe juga memakai header ini). Stok tetap 11.fingerprintbeda: key yang sama dipakai untuk body lain, misalnyaqty: 2. Itu kesalahan client, jadi ditolak422.saved.set(...)menyimpan response setelah pesanan dibuat, supaya retry berikutnya dapat jawaban yang sama.
Dengan key, tabel tadi berubah: retry pukul 10:20:10 dijawab 201 pesanan 501, stok tetap 11, saldo terpotong Rp89.000.
Kode status di sini mengikuti draft standar IETF untuk header Idempotency-Key: 422 kalau key yang sama datang dengan body berbeda, dan 409 Conflict kalau request pertama dengan key itu masih diproses waktu retry-nya datang (misalnya tombol ke-klik dua kali). Untuk 409, server menandai key-nya "sedang diproses" sebelum mulai bekerja. Banyak API pembayaran mewajibkan key-nya dan menjawab 400 kalau header itu gak ada.
Inti-nya cuma sebuah Map
Lepas dari Express, logikanya sederhana: key sudah tercatat, kembalikan yang disimpan; belum, buat, simpan, kembalikan.
function createOrderHandler() {
const store = new Map();
return {
order(key, qty) {
if (store.has(key)) {
return { ...store.get(key), replayed: true }; // salinan, yang disimpan gak diubah
}
const response = { id: "ord_" + store.size, qty, replayed: false };
store.set(key, response);
return response;
},
};
}
const h = createOrderHandler();
h.order("k1", 1); // { id: "ord_0", qty: 1, replayed: false }
h.order("k1", 1); // { id: "ord_0", qty: 1, replayed: true }
h.order("k2", 2); // { id: "ord_1", qty: 2, replayed: false }
"ord_" + store.sizedihitung sebelumstore.set: Map kosong berartiord_0, isi satu berartiord_1.{ ...store.get(key), replayed: true }membuat object baru. Kalau kamu menulisstore.get(key).replayed = true, response pertama ikut berubah jaditrue.- Versi mini ini belum membandingkan body. Server sungguhan membandingkannya dan menjawab
422.
Berapa lama key disimpan
Selamanya bikin storage bengkak. Stripe, misalnya, boleh menghapus key yang umurnya sudah lebih dari 24 jam. Jadi retry dua hari kemudian dengan key yang sama dianggap aksi baru. Client yang offline lama sebaiknya mengecek dulu, misalnya GET /orders?status=pending, sebelum mengirim ulang.
⚠️ Jebakan yang sering ketemu
- Membuat key baru di tiap retry.
crypto.randomUUID()di dalam loop retry bikin server mengira tiap percobaan aksi baru: tetap dobel. Buat key sekali per klik. - Memakai ulang key untuk aksi lain. Key 501 dipakai untuk beli
qty: 2:422. Aksi baru, key baru. - Mengubah response yang disimpan. Tandai replay di salinan (
{ ...saved, replayed: true }), jangan di object aslinya. - Mengira semua method perlu key.
PUTdanDELETEsudah idempoten. Yang perlu:POST, danPATCHyang menambah atau mengurangi.
TL;DR: Idempoten artinya dikirim berkali-kali, hasil akhirnya sama. GET, PUT, DELETE idempoten dari sananya; POST tidak, dan PATCH yang menambah (+1) juga tidak. Client yang kena timeout pasti retry, jadi POST /orders tanpa pengaman bikin pesanan dobel. Pengamannya header Idempotency-Key: client membuat satu UUID per aksi dan memakainya lagi saat retry; server menyimpan response per key dan memutar ulang response yang sama untuk retry. Key yang sama dengan body berbeda ditolak 422, dan key yang request pertamanya masih diproses dijawab 409. Kembalikan salinan saat replay, dan simpan key secukupnya, misalnya 24 jam.