Idempotency: Kata Keren yang Baru Kerasa Penting Pas Payment Lo Double
Gue baru serius soal idempotency setelah 1 callback payment bikin user ke-charge dua kali. Di dev semua kelihatan aman. Di production, retry, timeout, dan duplicate request bikin kekacauan.
0xNN · · 9 min read
Gue pertama kali ngerasa kata idempotency itu penting bukan pas baca dokumentasi. Tapi pas ada user yang kena charge dua kali gara-gara callback payment datang dobel dan kode gue terlalu polos buat bilang, "eh, request ini sebenernya udah pernah diproses."
Di local semuanya kelihatan baik-baik aja. Satu request masuk, order dibuat, payment sukses, invoice ke-generate. Tapi production gak hidup dalam dunia sebersih itu. Ada timeout, ada retry, ada webhook provider yang ngirim event yang sama dua kali, ada user yang klik tombol bayar dua kali karena UI lambat. Dan di situ lo baru sadar: request yang sama diproses dua kali bukan edge case. Itu traffic normal.
---
Idempotency itu apa, versi manusia
Definisi textbook-nya: operasi disebut idempotent kalau dieksekusi sekali atau berkali-kali hasil akhirnya tetap sama. Versi manusia: kalau request yang sama nyasar masuk lagi, sistem lo gak panik dan gak bikin side effect baru.
Contoh paling gampang: endpoint POST /payments. Tanpa idempotency, request pertama bikin charge Rp500.000. Request kedua yang identik juga bikin charge Rp500.000 lagi. Dari sudut pandang backend, dua-duanya valid. Dari sudut pandang user, lo baru bikin masalah.
Masalahnya sering bukan karena programmer "bodoh". Masalahnya karena banyak sistem distribusi memang didesain dengan prinsip at least once delivery. Webhook provider akan retry kalau response lo timeout. Mobile app akan retry kalau jaringan putus. Load balancer bisa memicu ulang request setelah koneksi reset. Jadi kalau backend lo menganggap duplicate request = mustahil, yang salah bukan infrastruktur. Yang salah asumsi lo.
---
Kenapa payment paling cepat ngajarin ini
Payment itu kejam karena side effect-nya nyata. Lo bisa salah render UI dan orang cuma kesel. Lo bisa salah proses payment dan orang langsung kehilangan uang.
Pola crash yang sering kejadian gini:
1. user klik tombol bayar
2. frontend kirim request
3. backend sukses charge ke provider
4. sebelum backend balas ke client, koneksi timeout
5. client retry request yang sama
6. backend charge lagi karena gak punya mekanisme idempotency
Atau versi webhook:
1. provider kirim event payment.succeeded
2. backend proses order, update status, kirim email
3. response 500 karena error di bagian email
4. provider retry event yang sama
5. backend proses ulang event sukses dan side effect dobel
Kalau lo gak simpan jejak request yang sudah diproses, sistem gak punya memori. Dia akan menganggap setiap request itu baru.
---
Implementasi paling umum: idempotency key
Pola yang paling waras biasanya pakai idempotency key. Client mengirim key unik, backend menyimpan hasil proses berdasarkan key itu. Kalau key yang sama datang lagi, backend balikin hasil lama, bukan bikin side effect baru.
POST /payments
Idempotency-Key: pay_20260816_order_123
Di server, logikanya kurang lebih:
CREATE TABLE idempotency_keys (
key text PRIMARY KEY,
request_hash text NOT NULL,
response_body jsonb NOT NULL,
status_code int NOT NULL,
created_at timestamptz NOT NULL default now()
);
Lalu alurnya:
• cek apakah key sudah ada
• kalau belum ada, proses request lalu simpan hasilnya
• kalau sudah ada dan payload-nya sama, balikin hasil lama
• kalau sudah ada tapi payload beda, tolak karena itu abuse atau bug client
Yang sering dilupain: jangan cuma simpan key. Simpan juga hash request-nya. Kalau enggak, client bisa ngirim key yang sama tapi nominal beda. Itu bukan retry aman. Itu request baru yang kebetulan numpang key lama.
---
Bukan cuma payment
Orang sering ngira idempotency itu cuma urusan payment API. Padahal banyak tempat lain yang butuh:
• create order
• create invoice
• kirim email penting
• provisioning resource cloud
• webhook consumer
• background job yang bisa retry
Bahkan worker queue juga butuh mindset yang sama. Job retry karena worker mati di tengah jalan itu normal. Kalau handler job lo gak aman dijalankan dua kali, lo bakal lihat data dobel, email dobel, atau status lompat-lompat.
Makanya gue sekarang lebih suka mikir begini: semua operasi yang punya side effect harus diasumsikan bisa dipanggil ulang. Kalau gak tahan dipanggil ulang, desainnya belum matang.
---
Yang sering disalahpahami
"Unique constraint aja cukup" - belum tentu. Unique constraint memang bisa nolong di level data, misalnya order_number gak boleh dobel. Tapi dia gak otomatis ngelindungin side effect lain kayak charge kartu, kirim email, publish event, atau tulis audit log dua kali.
"HTTP PUT kan idempotent, berarti aman" - secara semantik iya, secara implementasi belum tentu. Banyak API bilang idempotent, tapi kodenya masih punya side effect tambahan yang gak stabil. Jangan percaya method doang, cek implementasi.
"Idempotency bikin sistem lambat" - iya, ada biaya storage dan lookup. Tapi biaya itu jauh lebih murah daripada refund manual, tiket support, dan rusaknya trust user.
"Kalau retry jarang, gak perlu" - justru sistem yang retry-nya jarang bikin tim suka lengah. Begitu kejadian 1 kali di jalur payment atau billing, impact-nya langsung bisnis, bukan sekadar bug lucu.
---
Checklist implementasi yang waras
1. pakai idempotency key untuk endpoint create/payment/provisioning
2. simpan hash payload selain key-nya
3. simpan full response yang pernah berhasil dikembalikan
4. kasih TTL atau cleanup policy untuk key lama
5. buat processing atomic - insert key + side effect jangan balapan
6. anggap webhook dan background job itu at least once, bukan exactly once
7. log duplicate hit supaya lo tahu seberapa sering ini kejadian
Kalau gue lagi audit backend dan nemu endpoint payment tanpa idempotency, itu udah red flag. Bukan karena langsung pasti rusak. Tapi karena dia bergantung pada dunia yang terlalu ideal.
---
Referensi yang layak dibaca
• Stripe Docs - Idempotent requests
• PayPal Docs - PayPal-Request-Id
• AWS Builders' Library - Making retries safe with idempotent APIs
• RFC 9110 - HTTP Semantics
---
Intinya sederhana: duplicate request itu hal normal, bukan anomali. Yang bikin dia jadi bencana atau tidak adalah apakah backend lo punya memori buat bilang, "gue udah ngerjain ini tadi."
Gue pribadi baru berhenti meremehkan idempotency setelah lihat sendiri betapa cepatnya sistem yang kelihatannya rapi bisa bikin charge dobel cuma karena satu timeout dan satu retry. Sejak itu, setiap nulis endpoint yang bikin side effect, pertanyaan pertama gue bukan lagi "logic-nya benar gak?" Tapi: "kalau request ini masuk dua kali, apa yang terjadi?"
---
*ditulis setelah ngeliat callback payment dobel bikin sore yang harusnya santai berubah jadi sesi audit log, refund manual, dan telepon support yang gak lucu.*