Idempotency — API

Idempotency: memanggil operasi yang sama berkali-kali hasilnya sama seperti memanggil sekali. Konsep kritis untuk API yang reliable — tanpa ini, retry dari…

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:

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);
});

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 }

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

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.

Yang akan kamu pelajari