Idempotency key: kemenangan reliability paling murah
Cara membuat setiap mutating API endpoint aman untuk di-retry, dengan implementasi 50 baris yang langsung membayar dirinya sendiri.
Setiap API yang melibatkan uang, mengirim email, atau menciptakan side effect harus mendukung idempotency key. Ini salah satu fitur reliability paling murah yang bisa Anda tambah — dan salah satu yang paling under- implemented.
Masalahnya
Bayangkan Anda call API kami untuk membuat invoice. Anda kirim POST, network hiccup, Anda tidak dapat response. Apakah invoice-nya terbuat? Mungkin. Anda retry? Jika ya, mungkin Anda buat dua invoice. Jika tidak, mungkin tidak ada invoice yang terbuat.
Stripe mempopulerkan solusinya: client mengirim header Idempotency-Key di
setiap mutating request. Server menyimpan response pertama yang di-keyed
oleh header tersebut. Request berikutnya dengan key yang sama mengembalikan
response yang sudah di-cache, tanpa eksekusi ulang.
curl -X POST https://api.tenavora.com/v1/invoices \
-H "Idempotency-Key: 01HXPC..." \
-H "Content-Type: application/json" \
-d '{"amount": 50000}'
Retry call yang sama → response yang sama. Network hiccup → aman untuk di-retry. Tidak ada duplikasi invoice, tidak ada rekonsiliasi manual.
Cara kami implementasi
Middleware ini ada di Tenavora.Modules.AsyncReliability.Idempotency.
Alurnya:
- Baca header
Idempotency-Key. Kalau tidak ada, lewatkan tanpa perubahan. - Hash request body + path + method. Ini menjaga supaya client tidak reuse key yang sama untuk request yang berbeda — kami ingin gagal keras, bukan diam-diam mengembalikan response yang salah.
- Cari
(client_id, key)di tabelidempotency_records.- Ditemukan + completed: kembalikan cached status + body.
- Ditemukan + in-progress: kembalikan 409 dengan
Retry-After. (Race antara retry concurrent.) - Tidak ditemukan: insert row dengan state
in-progress, jalankan handler, update dengan response.
Tabelnya:
CREATE TABLE idempotency_records (
client_id uuid NOT NULL,
key text NOT NULL,
request_hash bytea NOT NULL,
status text NOT NULL CHECK (status IN ('in-progress','completed')),
response_status int,
response_body bytea,
created_at timestamptz NOT NULL DEFAULT now(),
completed_at timestamptz,
PRIMARY KEY (client_id, key)
);
CREATE INDEX idx_idempotency_expiry
ON idempotency_records (created_at);
Background job menyapu record yang lebih lama dari 24 jam. Client bisa retry dalam window itu; setelahnya kami anggap mereka sudah menyerah.
Edge case yang kami temui
-
Handler yang long-running: handler yang butuh 30 detik akan memblokir retry dengan 409 selama itu. Kami menambah header
Retry-Afterproporsional dengan waktu yang lewat supaya client back off dengan cerdas. -
Streaming response: response body yang besar tidak nyaman muat di kolom
response_body. Kami batasi di 1 MB dan return 422 kalau lewat (caller tidak seharusnya retry yang itu). -
Reuse key cross-tenant: key di-scope per client_id, jadi dua tenant bisa pakai string key yang sama tanpa collision. Penting — key biasanya UUID dari sisi client, tetapi memperlakukannya sebagai global akan jadi lubang privasi.
Kapan menggunakannya
Setiap mutating endpoint, idealnya. Kami menandainya wajib (via OpenAPI) di endpoint yang:
- Melibatkan uang atau mengubah state finansial
- Mengirim email, SMS, atau webhook
- Memicu workflow yang side-effect ke sistem eksternal
Untuk CRUD murni di mana membuat record yang sama tidak berbahaya, opsional. Untuk GET, tidak pernah — mereka idempotent by definition.
50 baris middleware, satu tabel, satu background job. Membayar dirinya sendiri pertama kali integrasi partner double-fire webhook.