Bayangkan seseorang menekan tombol “Bayar”, lalu halaman berhenti berputar. Karena tidak tahu apakah transaksi berhasil, ia menekan tombol itu lagi. Jika server langsung memproses setiap POST, satu pesanan bisa berubah menjadi dua pesanan, dua tagihan, atau dua pengiriman.
Masalah seperti ini bukan selalu disebabkan pengguna yang ceroboh. Request dapat diulang oleh browser, aplikasi mobile, reverse proxy, library HTTP, atau sistem retry ketika koneksi terputus setelah server menerima data tetapi sebelum client menerima respons.
Solusi yang umum dipakai untuk operasi penting adalah idempotency: request yang sama boleh dikirim beberapa kali, tetapi efek bisnisnya hanya terjadi satu kali. Dalam HTTP, GET, PUT, dan DELETE secara umum memiliki sifat idempotent, sedangkan POST tidak otomatis memilikinya. MDN menjelaskan perbedaan ini.
Apa itu idempotency key?
Idempotency key adalah identitas unik untuk satu niat operasi. Client membuat satu key ketika pengguna memulai pembayaran atau membuat pesanan, lalu mengirim key yang sama jika request perlu diulang.
Contohnya:
POST /api/orders
Idempotency-Key: 8b7d4f3e-4d8e-4f59-a0d1-2d2fd0c6a123Server menyimpan hubungan antara key tersebut dan hasil operasi. Ketika request dengan key yang sama datang lagi, server tidak membuat pesanan baru. Server cukup mengembalikan hasil yang sudah pernah dibuat.
Header Idempotency-Key kini juga didokumentasikan sebagai pola untuk membuat operasi POST atau PATCH lebih aman terhadap retry. Namun, dukungan dan aturan key tetap harus ditentukan oleh masing-masing API. Dokumentasi MDN dan dokumentasi Stripe menunjukkan pola tersebut.
Kapan fitur ini benar-benar dibutuhkan?
- Membuat pesanan atau invoice.
- Memproses pembayaran dan refund.
- Mengirim email, SMS, atau notifikasi berbayar.
- Mendaftarkan pengguna ke layanan berlangganan.
- Membuat tiket, reservasi, atau dokumen resmi.
- Memanggil API pihak ketiga yang memiliki efek finansial atau operasional.
Untuk endpoint pencarian atau pengambilan data, masalahnya biasanya bukan duplikasi efek bisnis. Tetapi untuk endpoint yang menciptakan sesuatu, retry tanpa perlindungan dapat menghasilkan data ganda.
Jangan hanya memeriksa key di aplikasi
Kesalahan umum adalah menyimpan idempotency key di cache tanpa aturan yang jelas, lalu langsung menjalankan proses bisnis setelah pemeriksaan sederhana:
if (!cache.has($key)) {
cache.set($key, true);
createOrder();
}Pola tersebut masih dapat bermasalah ketika dua request dengan key yang sama masuk hampir bersamaan. Keduanya bisa sama-sama membaca bahwa key belum ada sebelum salah satunya selesai menulis ke cache. Kondisi ini disebut race condition.
Untuk operasi penting, pemeriksaan duplikasi perlu dibantu oleh mekanisme atomik dan constraint database. Dengan kata lain, jangan hanya mengandalkan logika PHP; MySQL juga harus ikut menjaga keunikan data.
Contoh desain tabel di MySQL
CREATE TABLE idempotency_keys (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
idempotency_key VARCHAR( hundert ) NOT NULL,
request_hash CHAR(64) NOT NULL,
response_status SMALLINT NOT NULL,
response_body JSON NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_user_key (user_id, idempotency_key)
);Contoh di atas membutuhkan satu perbaikan kecil sebelum dipakai: pada MySQL, panjang kolom harus ditulis sebagai angka. Gunakan VARCHAR(100), bukan VARCHAR( hundert ). Bentuk yang benar adalah:
idempotency_key VARCHAR(100) NOT NULLKolom unik (user_id, idempotency_key) memastikan satu pengguna tidak dapat memiliki dua catatan dengan key yang sama. Beberapa sistem memilih key global, tetapi membatasi berdasarkan pengguna atau akun sering lebih sesuai dengan aturan keamanan aplikasi.
Alur pemrosesan yang lebih aman
- Client membuat key sekali. Key dibuat ketika pengguna memulai satu operasi, bukan setiap kali tombol diklik ulang.
- Server memvalidasi format dan autentikasi. Jangan biarkan key menjadi pengganti token login.
- Server menghitung hash request. Payload yang sama harus menghasilkan hash yang sama.
- Server mencoba membuat catatan idempotensi secara unik. Gunakan unique constraint atau operasi database atomik.
- Jika key sudah ada, bandingkan request hash. Key yang sama dengan isi berbeda harus ditolak, bukan diproses sebagai operasi baru.
- Jika key baru, proses bisnis dan simpan hasilnya dalam transaksi.
- Untuk retry, kembalikan status dan body yang sebelumnya disimpan.
Contoh respons ketika key yang sama digunakan untuk payload berbeda:
HTTP/1.1 409 Conflict
{
"error": "idempotency_key_reused",
"message": "Key sudah digunakan untuk request yang berbeda"
}Transaksi database tetap penting
Idempotency key tidak otomatis menyelesaikan semua masalah. Misalnya, server sudah menyimpan key tetapi gagal membuat order. Jika status key dianggap selesai, retry berikutnya mungkin hanya menerima hasil gagal tanpa kesempatan untuk melanjutkan.
Karena itu, simpan perubahan penting dalam transaksi yang konsisten. Catatan idempotency, order, dan perubahan stok perlu dirancang sesuai kebutuhan bisnis. Dalam beberapa kasus, status key dapat berupa processing, succeeded, atau failed. Status ini membantu server membedakan operasi yang masih berjalan dari operasi yang sudah selesai.
Namun, jangan menyimpan respons error sementara secara sembarangan. Error validasi dapat dikembalikan tanpa membuat operasi bisnis. Sebaliknya, error dari proses yang sudah benar-benar dieksekusi perlu memiliki aturan retry yang jelas.
Hal yang sering dilupakan
- Key harus cukup acak. UUID versi 4 atau random string dengan entropy memadai lebih aman daripada nomor urut sederhana.
- Key memiliki masa simpan. Simpan selama periode ketika retry masih masuk akal, lalu hapus dengan kebijakan retention yang jelas.
- Payload perlu diikat ke key. Tanpa request hash, pengguna dapat mengirim key lama dengan isi berbeda.
- Jangan mencatat data sensitif mentah. Respons yang disimpan perlu mengikuti aturan perlindungan data dan masking.
- Dokumentasikan perilaku retry. Client perlu tahu kapan harus menggunakan key lama dan kapan membuat key baru.
Apa artinya bagi kita?
Idempotency bukan fitur khusus untuk perusahaan besar. Website toko sederhana, sistem reservasi, dan aplikasi internal juga dapat mengalami request ganda. Jika sebuah endpoint bisa membuat uang, stok, tiket, atau pesan keluar dari sistem, endpoint itu layak diperlakukan sebagai operasi yang harus aman terhadap pengulangan.
Mulailah dari satu endpoint paling berisiko. Tambahkan header idempotency, unique constraint di MySQL, hash payload, dan pengujian dua request bersamaan. Setelah itu, ukur hasilnya melalui log: berapa banyak retry terjadi, berapa yang dikembalikan dari hasil lama, dan apakah ada konflik key.
Tujuan akhirnya bukan membuat semua request berjalan tanpa retry. Tujuannya adalah memastikan ketika jaringan tidak dapat memberi kepastian, sistem tetap dapat menjaga satu niat pengguna sebagai satu operasi bisnis.
Sumber & bacaan lebih lanjut
– Rio Yotto @rioyotto
