Idempotency key pada API POST dipakai untuk memastikan satu aksi bisnis hanya diproses sekali, meskipun client mengirim request yang sama berulang karena timeout, retry, atau koneksi putus. Tanpa mekanisme ini, API bisa membuat order ganda, charge pembayaran dua kali, atau mengirim notifikasi berulang.
Masalahnya bukan hanya teknis. Request ganda langsung berdampak ke bisnis: stok bisa berkurang dua kali, invoice terbit dua kali, saldo terpotong dua kali, dan tim support harus melakukan rekonsiliasi manual. Karena itu, endpoint POST yang memicu efek samping penting sering perlu perlindungan idempotency, terutama pada operasi create, payment, checkout, booking, dan submit transaksi.
Apa itu idempotency key dan kapan dibutuhkan
Secara sederhana, idempotency key adalah identifier unik yang dikirim client bersama request POST. Server menyimpan hasil pemrosesan untuk key tersebut. Jika request yang sama datang lagi dengan key yang sama, server tidak menjalankan aksi bisnis kedua kalinya, tetapi mengembalikan hasil yang sudah tersimpan.
Idempotency key paling relevan saat:
- Client retry otomatis karena timeout atau error jaringan.
- Mobile app kehilangan koneksi saat request sedang diproses.
- Gateway/proxy melakukan retry di layer jaringan.
- User menekan tombol submit berulang karena respons terasa lambat.
- Operasi finansial atau transaksi tidak boleh dijalankan dua kali.
Kapan POST perlu idempotency
Tidak semua POST wajib memakai idempotency key. Gunakan jika request:
- membuat resource baru yang tidak boleh duplikat,
- menjalankan efek samping eksternal seperti charge payment atau kirim email penting,
- mahal untuk diulang,
- sulit di-roll back jika diproses dua kali.
Untuk POST yang sifatnya non-kritis, misalnya log event analitik yang memang boleh terduplikasi dan diproses secara eventual, idempotency key bisa jadi tidak perlu.
Perbedaan dengan PUT
PUT secara semantik HTTP dirancang idempotent: mengirim request yang sama berkali-kali ke resource yang sama seharusnya menghasilkan keadaan akhir yang sama. Contohnya, PUT /users/123 untuk mengganti profil user.
POST berbeda. POST biasanya dipakai untuk membuat resource baru atau menjalankan action yang efeknya tidak otomatis idempotent. Contohnya, POST /orders bisa membuat order baru setiap kali dipanggil. Karena itulah POST sering membutuhkan idempotency key jika retry dapat terjadi.
Catatan: Idempotency key bukan pengganti desain resource yang baik. Jika operasi sebenarnya lebih cocok sebagai PUT dengan identifier yang sudah diketahui client, PUT mungkin menjadi desain yang lebih sederhana.
Alur request pertama vs retry
Request pertama
- Client membuat key unik, misalnya UUID.
- Client mengirim POST dengan header
Idempotency-Key. - Server memeriksa apakah key sudah pernah dipakai dalam scope yang relevan.
- Jika belum ada, server menandai request sebagai sedang diproses, menjalankan logika bisnis, lalu menyimpan response final.
- Server mengembalikan hasil ke client.
Request retry dengan key yang sama
- Server mencari key yang sama.
- Jika hasil final sudah tersimpan, server mengembalikan response yang sama tanpa menjalankan aksi bisnis lagi.
- Jika request pertama masih diproses, server bisa mengembalikan status bahwa request sedang berlangsung, atau menunggu sebentar lalu mengembalikan hasil jika sudah selesai.
Intinya, retry harus menghasilkan satu efek bisnis, bukan satu eksekusi bisnis per attempt.
Desain idempotency key yang benar
Format key
Format paling umum adalah string acak yang cukup unik, misalnya UUID atau token acak kriptografis. Server tidak perlu menebak arti key dari formatnya; yang penting unik dan aman dipakai sebagai identifier request.
Contoh header:
POST /v1/orders HTTP/1.1
Idempotency-Key: 8f5d0f55-4d59-4f7b-ae2c-5c2f9a6e7b19
Content-Type: application/jsonPraktik yang disarankan:
- Gunakan key yang dihasilkan client, bukan server.
- Batasi panjang maksimum key untuk mencegah abuse.
- Validasi karakter yang diizinkan jika perlu.
- Jangan memakai key yang mudah ditebak jika dapat dieksploitasi lintas user.
Scope key: jangan global tanpa konteks
Key sebaiknya tidak dianggap unik secara global tanpa batas. Umumnya scope terbaik adalah kombinasi:
- tenant/user/account,
- HTTP method,
- route atau operasi.
Contoh: key abc-123 milik user A untuk POST /payments tidak boleh bentrok dengan key yang sama milik user B atau endpoint lain seperti POST /orders.
Secara konseptual, identitas unik record menjadi:
(owner_id, method, route, idempotency_key)Ikat key ke payload
Ini bagian yang sering terlewat. Jika server hanya mengecek key tanpa mengikatnya ke payload, client bisa mengirim key yang sama dengan isi request berbeda. Itu berbahaya karena server mungkin mengembalikan response lama untuk payload baru, atau justru menimbulkan inkonsistensi.
Solusi umum:
- Simpan hash payload yang dinormalisasi.
- Saat key yang sama datang lagi, bandingkan hash payload.
- Jika berbeda, kembalikan error conflict.
Payload perlu dinormalisasi secara konsisten sebelum di-hash, misalnya serialisasi JSON yang stabil. Tujuannya agar perbedaan urutan field yang tidak bermakna tidak dianggap sebagai request berbeda.
TTL: berapa lama key disimpan
Idempotency key biasanya tidak disimpan selamanya. Gunakan TTL sesuai karakteristik bisnis:
- Beberapa jam sampai beberapa hari untuk transaksi normal.
- Lebih lama untuk pembayaran atau proses yang sering di-retry dari client mobile.
Pertimbangannya:
- Terlalu pendek: retry yang valid datang setelah key expired dan request diproses ulang.
- Terlalu panjang: storage membengkak dan reuse key lama jadi membingungkan.
Tidak ada angka universal. Pilih TTL berdasarkan jendela retry realistis, SLA client, dan risiko duplikasi pada domain Anda.
Status code dan perilaku response
Prinsip utama: jika retry dengan key dan payload yang sama datang setelah request pertama sukses, server sebaiknya mengembalikan response yang konsisten dengan hasil pertama.
Status yang umum dipakai
- 201 Created untuk request pertama yang benar-benar membuat resource.
- 200 OK atau 201 Created juga bisa dipakai untuk retry sukses, selama perilakunya konsisten dan terdokumentasi.
- 409 Conflict jika key yang sama dipakai dengan payload berbeda.
- 202 Accepted atau 409/429 dengan pesan sedang diproses jika request identik masih in-flight, tergantung desain API.
- 400 Bad Request jika header key diwajibkan tetapi tidak ada atau formatnya tidak valid.
Yang paling penting bukan sekadar kode status, tetapi aturan yang jelas dan konsisten di semua retry scenario.
Saran praktis: simpan dan kembalikan kembali status code + response body utama dari request pertama. Dengan begitu, retry tidak perlu menebak hasil akhir.
Penyimpanan idempotency record
Data minimal yang biasanya perlu disimpan:
- scope: user/tenant, method, route, idempotency_key,
- payload_hash,
- status pemrosesan: processing/succeeded/failed,
- HTTP status code,
- response body atau referensi ke resource yang dibuat,
- waktu kedaluwarsa,
- metadata debug seperti request_id dan created_at.
Contoh skema tabel database
CREATE TABLE idempotency_keys (
id BIGINT PRIMARY KEY,
owner_id BIGINT NOT NULL,
http_method VARCHAR(10) NOT NULL,
route_key VARCHAR(100) NOT NULL,
idempotency_key VARCHAR(255) NOT NULL,
payload_hash VARCHAR(128) NOT NULL,
status VARCHAR(20) NOT NULL,
response_status SMALLINT,
response_body TEXT,
resource_type VARCHAR(50),
resource_id VARCHAR(64),
locked_until TIMESTAMP NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
UNIQUE (owner_id, http_method, route_key, idempotency_key)
);Catatan:
response_bodybisa diganti dengan pointer ke resource untuk menghemat storage.locked_untilberguna untuk menandai request yang sedang diproses dan mencegah race condition.- Simpan
payload_hash, bukan payload mentah, jika payload sensitif atau besar.
Contoh struktur Redis
Key:
idem:{owner_id}:{method}:{route}:{idempotency_key}
Value (JSON):
{
"payload_hash": "sha256:...",
"status": "processing",
"response_status": 201,
"response_body": {"order_id": "ord_123", "status": "created"},
"resource_id": "ord_123",
"expires_at": "2026-04-12T10:00:00Z"
}Redis cocok jika Anda butuh lookup cepat dan TTL natural. Database relasional lebih nyaman jika Anda perlu audit, query, dan integritas transaksi kuat. Banyak sistem memakai kombinasi keduanya: Redis untuk guard cepat, database untuk sumber kebenaran.
Race condition dan concurrent request
Masalah tersulit biasanya bukan retry berurutan, tetapi dua request identik yang datang hampir bersamaan. Misalnya user menekan tombol dua kali, atau gateway melakukan retry sebelum request pertama selesai.
Jika implementasi hanya check then insert tanpa atomicity, dua request bisa sama-sama melihat key belum ada lalu sama-sama mengeksekusi logika bisnis. Inilah race condition klasik.
Cara mencegahnya
- Gunakan unique constraint pada scope key di database.
- Gunakan operasi atomik seperti
INSERT ... ON CONFLICT,SETNXdi Redis, atau lock transaksional. - Tandai status
processingsebelum logika bisnis dijalankan. - Pastikan finalisasi status ke
succeededdilakukan setelah aksi bisnis sukses.
Pseudocode middleware idempotency
function handleRequest(request):
key = request.header["Idempotency-Key"]
if key is missing:
return 400
scope = buildScope(request.userId, request.method, request.route)
payloadHash = hash(normalize(request.body))
record = store.find(scope, key)
if record exists:
if record.payloadHash != payloadHash:
return 409 with error "Idempotency key reused with different payload"
if record.status == "succeeded":
return response(record.responseStatus, record.responseBody)
if record.status == "processing":
return 202 with error "Request is being processed"
acquired = store.tryCreateProcessingRecord(scope, key, payloadHash, ttl)
if not acquired:
record = store.find(scope, key)
if record.payloadHash != payloadHash:
return 409
if record.status == "succeeded":
return response(record.responseStatus, record.responseBody)
return 202
try:
result = executeBusinessLogic(request)
store.markSucceeded(scope, key, result.status, result.body)
return response(result.status, result.body)
catch err:
store.markFailedOrDelete(scope, key)
throw errPoin penting pada pseudocode di atas:
- Pengecekan payload dilakukan pada setiap reuse key.
- Pembuatan record
processingharus atomik. - Response sukses disimpan agar retry bisa mendapatkan hasil yang sama.
Contoh request dan response
Request pertama
POST /v1/orders HTTP/1.1
Authorization: Bearer <token>
Idempotency-Key: 7e9a4c6f-8b0f-4a64-8a44-12f4b9cbfd90
Content-Type: application/json
{
"customer_id": "cust_123",
"items": [
{"sku": "SKU-1", "qty": 2}
],
"payment_method": "card"
}HTTP/1.1 201 Created
Content-Type: application/json
{
"order_id": "ord_987",
"status": "created",
"amount": 250000
}Retry setelah timeout di client
Client tidak tahu apakah request pertama sukses atau tidak, lalu mengirim ulang request yang sama dengan key yang sama.
POST /v1/orders HTTP/1.1
Authorization: Bearer <token>
Idempotency-Key: 7e9a4c6f-8b0f-4a64-8a44-12f4b9cbfd90
Content-Type: application/json
{
"customer_id": "cust_123",
"items": [
{"sku": "SKU-1", "qty": 2}
],
"payment_method": "card"
}HTTP/1.1 201 Created
Content-Type: application/json
{
"order_id": "ord_987",
"status": "created",
"amount": 250000
}Server tidak membuat order baru. Ia hanya mengembalikan hasil yang sama.
Key sama, payload berbeda
POST /v1/orders HTTP/1.1
Authorization: Bearer <token>
Idempotency-Key: 7e9a4c6f-8b0f-4a64-8a44-12f4b9cbfd90
Content-Type: application/json
{
"customer_id": "cust_123",
"items": [
{"sku": "SKU-1", "qty": 3}
],
"payment_method": "card"
}HTTP/1.1 409 Conflict
Content-Type: application/json
{
"error": "idempotency_key_conflict",
"message": "Key yang sama tidak boleh dipakai untuk payload berbeda"
}Strategi penyimpanan response: body penuh atau referensi resource?
Simpan response penuh
Kelebihan:
- Mudah mengembalikan respons identik pada retry.
- Tidak perlu membangun ulang body dari sumber lain.
Kekurangan:
- Storage lebih besar.
- Berisiko menyimpan data sensitif jika tidak difilter.
Simpan referensi resource
Misalnya hanya simpan resource_type=order dan resource_id=ord_987, lalu response dibangun ulang saat retry.
Kelebihan:
- Lebih hemat storage.
- Lebih mudah menjaga data sensitif.
Kekurangan:
- Response retry bisa sedikit berbeda jika representasi resource berubah.
- Lebih rumit untuk response yang bukan sekadar representasi resource.
Untuk banyak API internal, menyimpan status code dan response body yang sudah disanitasi sering menjadi pilihan paling sederhana dan aman secara operasional.
Apa yang dilakukan saat pemrosesan gagal?
Ini keputusan desain yang perlu eksplisit. Jika logika bisnis gagal karena error internal, ada dua pendekatan umum:
- Hapus record processing agar client bisa retry dari awal.
- Simpan hasil gagal tertentu jika kegagalan itu merupakan hasil final yang ingin diulang secara konsisten.
Biasanya:
- Untuk validation error atau business rule error, hasil gagal dapat disimpan karena retry dengan payload yang sama akan tetap gagal.
- Untuk transient error seperti gangguan database sementara, record bisa dilepas agar retry benar-benar mencoba lagi.
Yang penting adalah membedakan antara final failure dan transient failure.
Kesalahan umum dalam implementasi
1. Menyimpan key tanpa response
Jika server hanya menyimpan bahwa key sudah pernah dipakai, tetapi tidak menyimpan hasilnya, retry tidak tahu harus mengembalikan apa. Akibatnya Anda tetap harus mengeksekusi query tambahan atau, lebih buruk, tidak bisa memastikan response konsisten.
2. TTL terlalu pendek
TTL yang terlalu agresif membuat key hilang sebelum retry yang valid datang. Ini sering terjadi pada aplikasi mobile dengan koneksi tidak stabil atau proses bisnis yang lama.
3. Tidak mengikat key ke payload
Ini membuka celah conflict serius. Key yang sama bisa dipakai ulang untuk isi request berbeda dan server salah menganggapnya request yang sama.
4. Tidak menangani concurrent request
Cek eksistensi key saja tidak cukup. Tanpa insert atomik, dua request paralel tetap bisa lolos dan sama-sama memproses aksi bisnis.
5. Scope key terlalu luas atau terlalu sempit
Jika global, collision antar endpoint atau user bisa terjadi. Jika terlalu sempit dan tidak konsisten, key yang seharusnya mewakili aksi yang sama malah tidak terdeteksi.
6. Menyimpan payload sensitif mentah
Untuk kebutuhan deduplikasi, hash payload sering cukup. Hindari menyimpan data kartu, token, atau informasi sensitif yang tidak perlu.
Checklist implementasi idempotency key
- Tentukan endpoint POST mana yang berisiko duplikasi dan wajib idempotent.
- Wajibkan header
Idempotency-Keypada endpoint kritis. - Tentukan scope unik: user/tenant + method + route + key.
- Normalisasi payload lalu simpan hash-nya.
- Buat unique constraint atau operasi atomik untuk mencegah race condition.
- Simpan status
processingsebelum logika bisnis berjalan. - Simpan
status codedanresponsefinal, atau referensi ke resource yang dibuat. - Tentukan TTL berdasarkan pola retry nyata di sistem Anda.
- Definisikan aturan jika key sama dipakai dengan payload berbeda: biasanya
409 Conflict. - Definisikan perilaku untuk request yang masih in-flight.
- Bedakan penanganan final failure vs transient failure.
- Tambahkan logging dengan request ID, key, dan hasil deduplikasi untuk debugging.
- Uji skenario timeout, retry paralel, duplicate submit, dan expiry key.
Debugging dan observability
Jika implementasi idempotency terasa “kadang berhasil, kadang tidak”, biasanya masalah ada pada observability yang kurang. Log minimal yang berguna:
- idempotency_key,
- owner_id/tenant_id,
- route dan method,
- payload_hash,
- status record: processing/succeeded/failed,
- apakah request hasil dedupe atau eksekusi baru,
- resource_id yang dihasilkan.
Tambahkan metrik seperti:
- jumlah request dengan idempotency key,
- jumlah deduplicated retry,
- jumlah conflict payload,
- jumlah concurrent in-flight collision.
Metrik ini membantu melihat apakah retry memang sering terjadi dan apakah TTL atau desain scope sudah tepat.
Penutup
Idempotency key pada API POST adalah pola praktis untuk mencegah duplikasi saat client retry karena timeout, koneksi putus, atau submit berulang. Implementasi yang benar bukan sekadar menyimpan key, tetapi juga mencakup scope yang tepat, hash payload, penyimpanan hasil response, TTL yang masuk akal, dan perlindungan terhadap race condition.
Jika endpoint POST Anda memicu efek bisnis yang mahal atau tidak boleh terulang, tambahkan idempotency sejak awal. Biayanya kecil dibandingkan konsekuensi order ganda, pembayaran dobel, dan debugging insiden produksi yang seharusnya bisa dicegah.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!