Jadikan efek idempoten
Rancang kunci idempotensi yang tepat cakupan, cocokkan payload, simpan efek dan hasil secara atomik, dan jelaskan sampai kapan jaminannya berlaku.
Pertanyaan
Bagaimana kamu menjamin bahwa request yang dikirim dua kali, atau yang responsnya hilang, hanya menimbulkan satu efek bisnis?
Selesai baca ini, kamu bisa merancang kunci idempotensi, memutuskan apa yang disimpan dan di mana, menangani retry yang datang bersamaan, dan menyebut batas masa berlakunya.
Prasyarat dan contoh
Kamu paham transaksi database dasar (commit dan rollback) serta M03.03 dan M03.04. Detail isolasi transaksi baru dibahas di M05, di sini cukup tahu bahwa satu transaksi bisa menyimpan beberapa baris secara atomik.
Contoh: POST /payments menagih Rp125.000. Server memproses, commit ke database, lalu koneksi
putus sebelum respons sampai ke klien. Klien melihat timeout. Ia nggak tahu apakah tagihan
terjadi. Kalau klien nggak retry, mungkin pelanggan sudah tertagih tanpa pesanan. Kalau klien
retry tanpa perlindungan, pelanggan tertagih dua kali.
Diam di tengah ketidakpastian itu sah. Yang dibutuhkan bukan "jangan pernah retry", tapi "retry yang hasilnya sama dengan sekali jalan".
Mekanisme
Klien membuat kunci unik per niat (misalnya UUID) dan mengirimnya di header
Idempotency-Key. Retry memakai kunci yang sama. Server menyimpan hasil pertama dan
mengembalikannya untuk pengulangan.
1. Cakupan key. Key harus dicakup agar nggak bentrok: (tenant_id, endpoint, key).
Tanpa tenant, dua pelanggan yang kebetulan membuat key sama saling membaca hasil. Tanpa
endpoint, key yang dipakai di /payments bisa mengembalikan respons dari /refunds.
2. Kecocokan payload. Simpan hash dari request (method, path, body yang dinormalkan) di
samping key. Kalau key sama datang dengan payload berbeda (misalnya jumlah 125.000 lalu
200.000), itu bug klien, bukan retry. Tolak dengan 422 atau 409, jangan kembalikan hasil
lama untuk request yang berbeda.
3. Efek dan hasil disimpan atomik. Jangan dua penulisan terpisah:
SALAH: 1) catat key 2) lakukan efek 3) simpan hasil
BENAR: BEGIN
INSERT idempotency (tenant, endpoint, key, request_hash) -- unik
INSERT payment ...
UPDATE idempotency SET response = ..., status = 'done'
COMMIT
Kalau key dicatat lalu server crash sebelum efek terjadi, retry melihat "key sudah ada" dan mengira pekerjaan selesai padahal nggak ada efek. Kalau efek terjadi lalu crash sebelum hasil dicatat, retry mengulang efek. Dengan satu transaksi, kedua hal itu muncul bersama atau nggak sama sekali.
4. Retry bersamaan. Dua request dengan key sama tiba di detik yang sama. Constraint
unik pada (tenant, endpoint, key) memastikan hanya satu INSERT yang menang. Yang kalah
punya tiga pilihan: menunggu transaksi pemenang selesai lalu mengembalikan hasilnya, membalas
409 Conflict ("sedang diproses, coba lagi"), atau memeriksa status baris in_progress.
Jangan menjalankan efek dua kali "sambil berharap".
5. Masa simpan. Key nggak disimpan selamanya. Contoh: 24 jam. Artinya jaminan hanya berlaku dalam 24 jam sejak percobaan pertama. Retry setelah 25 jam dengan key yang sama dianggap request baru dan menagih lagi. Tulis ini di kontrak, dan pilih durasi lebih panjang dari retry terlama yang mungkin dilakukan klien (termasuk antrean offline).
6. Batas efek eksternal. Transaksi database nggak menjangkau provider pembayaran. Kalau kamu memanggil provider lalu crash sebelum commit, efek eksternal sudah terjadi tanpa catatan lokal. Solusi umum: teruskan key idempotensi ke provider (dia punya mekanisme serupa) dan turunkan key itu secara deterministik dari key milik kamu. Kalau provider nggak mendukungnya, kamu butuh rekonsiliasi: periksa status di provider sebelum mengulang, dan jelaskan sisa risikonya.
Saat rancangan ini gagal
- Dua penulisan terpisah. Key di Redis, efek di Postgres. Crash di antaranya membuat keduanya nggak konsisten, dan nggak ada urutan penulisan yang aman.
- Key lintas pengguna. Cakupan tanpa tenant: pelanggan A mengirim key yang sama persis
dengan B (misalnya
"1") dan menerima respons milik B, termasuk data pribadinya. - Payload nggak dibandingkan. Klien memakai ulang key untuk pesanan berbeda. Server dengan tenang mengembalikan hasil pesanan pertama, dan pesanan kedua nggak pernah dibuat.
- Respons lama dengan status salah. Request pertama gagal (
500) setelah efek parsial. Menyimpan500sebagai hasil final mengunci kegagalan. Simpan hasil hanya setelah efek berhasil, atau tandai kegagalan sementara supaya bisa diulang. - Retensi lebih pendek dari retry. Key hidup 1 jam, tapi aplikasi mobile bisa mengirim ulang antrean offline besok pagi. Retry itu menggandakan efek tanpa ada yang melanggar kontrak di sisi server, karena kontraknya memang cuma 1 jam.
- Efek eksternal. Email yang terkirim dua kali dan nggak bisa ditarik. Idempotensi lokal nggak menolong, yang bisa dilakukan hanya mengurangi peluang dan menerima sisanya.
Latihan
POST /transfers memindahkan uang antar rekening. Key idempotensi disimpan 48 jam dengan
constraint unik pada (account_id, key).
- Klien mengirim key
K1, jumlah 100.000. Respons hilang. Klien retry denganK1, jumlah 100.000. Apa yang dilakukan server? - Klien yang berbeda bug-nya mengirim
K1dengan jumlah 150.000 untuk rekening yang sama. - Dua retry
K1tiba bersamaan, sementara pemrosesan pertama masih berjalan. - Retry
K1datang 60 jam kemudian. - Apa yang harus ada dalam satu transaksi, dan kenapa?
Lihat pembahasan
- Server menemukan
(account, K1)dengan hash payload sama dan status selesai, lalu mengembalikan respons tersimpan (status dan body yang sama dengan percobaan pertama). Uang nggak bergerak lagi. - Hash payload berbeda dengan yang tersimpan. Tolak dengan
422(atau409) dan pesan bahwa key dipakai dengan request lain. Jangan mengembalikan hasil Rp100.000 untuk permintaan Rp150.000. - Constraint unik membuat hanya satu yang menjadi pemroses. Request lainnya melihat baris
in_progressdan menunggu singkat atau membalas409dengan saran retry. Tidak ada yang menjalankan transfer kedua. - Setelah 48 jam, key sudah dibersihkan, jadi
K1diperlakukan sebagai request baru dan transfer terjadi lagi. Ini batas jaminan yang harus tertulis di kontrak. Mitigasi opsional: untuk operasi bernilai besar, tambahkan constraint bisnis (misalnya nomor referensi unik dari klien) yang berlaku lebih lama dari key. - Insert baris idempotensi, mutasi saldo kedua rekening dan entri transfer, serta update baris idempotensi dengan respons akhir. Kalau salah satunya berada di luar transaksi, crash di tengah menghasilkan salah satu dari dua keadaan buruk: efek terjadi tanpa hasil tersimpan, atau hasil tersimpan tanpa efek.
Terkait
- M03.04 Lakukan retry dengan bertanggung jawab menentukan kapan retry terjadi, dan lesson ini membuatnya aman.
- Atomicity transaksi, anomali isolasi, dan outbox dibahas di M05 yang belum tersedia.
- Pemrosesan pesan dengan consumer idempoten dibahas di M10, juga belum tersedia.
- Proyek B01, lab L03, rancangan D05, dan studi kasus C01 (kontrak idempotensi Stripe) belum tersedia.
Catatan
- Asumsi: satu database yang mendukung transaksi dan constraint unik menyimpan efek dan key.
- Header
Idempotency-Keyadalah konvensi yang dipakai banyak API pembayaran. Ada draf standar IETF, tapi belum semua API mengikutinya. Nama header dan masa simpan itu pilihan kontrak kamu. - "Exactly-once" lintas sistem nggak dijamin di sini. Yang dijamin hanya satu efek lokal per key selama masa simpan.
- Terakhir ditinjau: 26 Oktober 2026.