Bayangkan pengguna menekan tombol “Bayar”, lalu koneksi internet terputus sebelum halaman menerima jawaban dari server. Apakah pembayaran berhasil atau belum? Karena tidak tahu, aplikasi mungkin mencoba mengirim permintaan yang sama sekali lagi. Jika server langsung memproses kedua permintaan itu, hasilnya bisa berupa dua pesanan, dua pengurangan stok, atau dua transaksi.
Masalah seperti ini bukan hanya terjadi pada sistem pembayaran besar. Form pemesanan, pembuatan tiket, pengiriman email, pendaftaran anggota, dan proses lain yang mengubah data juga bisa mengalaminya. Solusinya bukan sekadar melarang retry, melainkan membuat endpoint mampu mengenali permintaan yang sama. Pola ini dikenal sebagai idempotency.
Apa arti idempotency?
Secara sederhana, operasi idempotent adalah operasi yang tetap menghasilkan efek utama yang sama meskipun permintaan identik dikirim lebih dari sekali. HTTP mendefinisikan beberapa metode seperti GET, PUT, dan DELETE sebagai idempotent berdasarkan efek yang diminta ke server. Namun, POST biasanya tidak otomatis idempotent karena setiap panggilan dapat membuat sumber daya baru.
Contohnya, memanggil PUT /users/15 dengan data nama yang sama berkali-kali seharusnya tetap menghasilkan satu kondisi akhir yang sama. Sebaliknya, memanggil POST /orders tiga kali bisa membuat tiga pesanan jika server tidak memiliki mekanisme pencegah duplikasi.
Perlu dicatat, idempotent bukan berarti server tidak melakukan efek samping apa pun. Server tetap boleh mencatat log atau statistik setiap permintaan. Yang penting, efek bisnis utamanya tidak berlipat ganda.
Kenapa retry tidak bisa dihindari?
Dalam sistem web, kegagalan tidak selalu berarti operasi di server gagal. Bisa saja server sudah menyimpan pesanan, tetapi responsnya hilang ketika jaringan terputus. Dari sisi klien, situasinya terlihat seperti “belum berhasil”, padahal server sudah bekerja.
Retry juga dapat terjadi karena:
- pengguna menekan tombol dua kali karena halaman tampak tidak merespons;
- browser atau aplikasi mengulangi request setelah koneksi bermasalah;
- layanan perantara seperti queue atau worker menjalankan ulang pekerjaan yang dianggap gagal;
- timeout terjadi sebelum klien menerima respons;
- pengguna membuka ulang halaman konfirmasi dan mengirim data kembali.
Karena itu, endpoint yang membuat data penting harus dirancang dengan asumsi bahwa request dapat diterima lebih dari sekali.
Gunakan idempotency key pada operasi penting
Idempotency key adalah tanda unik yang dikirim klien untuk menandai satu niat bisnis. Misalnya, ketika pengguna membuat pesanan, aplikasi membuat key seperti order-8f2c.... Jika request dikirim ulang dengan key yang sama, server tidak membuat pesanan baru. Server cukup mengembalikan hasil dari pemrosesan pertama.
Contoh request:
POST /api/orders HTTP/1.1
Content-Type: application/json
Idempotency-Key: 8f2c6f36-2d4a-4c7a-9b67-1b1d8c9e1234
{
"product_id": 42,
"quantity": 1
}Key tersebut sebaiknya dibuat oleh klien untuk satu tindakan pengguna, bukan dibuat ulang setiap kali fungsi retry dijalankan. Dengan begitu, percobaan pertama dan percobaan kedua tetap dikenali sebagai operasi yang sama.
Simpan hasilnya, bukan hanya key-nya
Kesalahan umum adalah menyimpan idempotency key sebagai penanda bahwa request pernah masuk, tetapi tidak menyimpan hasil responsnya. Akibatnya, request kedua memang tidak membuat data baru, tetapi klien tidak mendapatkan informasi pesanan yang sudah dibuat.
Di database, kita bisa membuat tabel sederhana:
CREATE TABLE idempotency_keys (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
request_key VARCHAR(100) NOT NULL,
request_hash CHAR(64) NOT NULL,
response_code SMALLINT NOT NULL,
response_body JSON NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_user_request (user_id, request_key)
);request_hash berguna untuk memastikan satu key tidak dipakai ulang untuk isi permintaan yang berbeda. Jika key yang sama tiba-tiba dipakai untuk produk atau jumlah yang berbeda, server sebaiknya menolak request tersebut. Ini mencegah bug klien dan penyalahgunaan key.
Urutan proses yang aman di PHP dan MySQL
Implementasi praktisnya perlu melibatkan transaksi database. Alur sederhananya seperti ini:
- Ambil
Idempotency-Keydari header dan validasi formatnya. - Hitung hash dari payload yang sudah dinormalisasi.
- Cari key tersebut berdasarkan pengguna atau konteks akun.
- Jika sudah ada, bandingkan hash payload. Jika sama, kembalikan respons tersimpan. Jika berbeda, kembalikan error.
- Jika belum ada, mulai transaction.
- Buat pesanan, kurangi stok, atau jalankan perubahan bisnis lain.
- Simpan hasil respons ke tabel idempotency.
- Commit transaction, lalu kirim respons ke klien.
Contoh pseudocode PHP:
$key = $_SERVER['HTTP_IDEMPOTENCY_KEY'] ?? '';
$payload = getNormalizedJsonBody();
$hash = hash('sha256', json_encode($payload));
$db->beginTransaction();
$record = findIdempotencyKey($userId, $key);
if ($record) {
if (!hash_equals($record['request_hash'], $hash)) {
throw new ApiException(409, 'Key sudah dipakai untuk payload berbeda');
}
$db->commit();
return jsonResponse($record['response_body'], $record['response_code']);
}
$orderId = createOrderAndUpdateStock($payload);
$response = ['order_id' => $orderId, 'status' => 'created'];
saveIdempotencyResult($userId, $key, $hash, 201, $response);
$db->commit();
return jsonResponse($response, 201);Dalam implementasi nyata, penanganan error juga penting. Jika proses bisnis gagal dan transaction di-rollback, hasil kegagalan belum tentu perlu disimpan. Namun, aturan ini harus konsisten. Beberapa layanan memilih menyimpan respons error tertentu agar retry berikutnya mendapat hasil yang sama, sementara error validasi biasanya tidak disimpan karena request memang belum diproses.
Jangan lupa masalah request bersamaan
Dua request dengan key sama bisa tiba hampir bersamaan. Jika keduanya lebih dulu memeriksa tabel lalu sama-sama menemukan bahwa key belum ada, keduanya dapat melanjutkan proses.
Di sinilah unique index dan transaction menjadi penting. Database harus menjadi pengaman terakhir, bukan hanya logika PHP. Gunakan constraint unik, tangani duplicate key error, dan pastikan operasi pembuatan pesanan serta pencatatan key berada dalam transaction yang sesuai.
Selain itu, hindari menyimpan key tanpa batas waktu. Data idempotency dapat dibersihkan setelah periode tertentu, misalnya beberapa jam atau beberapa hari, sesuai risiko bisnis. Periode tersebut harus cukup panjang untuk mengatasi retry yang masuk akal, tetapi tidak membuat tabel tumbuh tanpa kendali.
Yang bisa dilakukan sekarang
- Identifikasi endpoint yang membuat pesanan, pembayaran, tiket, akun, atau perubahan penting.
- Tentukan apakah endpoint tersebut aman untuk diulang atau memerlukan idempotency key.
- Tambahkan unique constraint di database, bukan hanya pemeriksaan di kode aplikasi.
- Simpan payload hash dan respons agar retry bisa mendapatkan hasil yang konsisten.
- Uji skenario timeout, tombol diklik dua kali, request bersamaan, dan koneksi putus setelah commit.
- Catat key di log dengan hati-hati, tetapi jangan memasukkan data sensitif ke dalam key.
Idempotency bukan pengganti transaction, validasi, atau kontrol stok. Ia adalah lapisan tambahan untuk menghadapi kenyataan bahwa jaringan dan klien tidak selalu bisa memberi tahu apakah operasi sudah berhasil. Dengan desain ini, retry berubah dari sumber duplikasi menjadi mekanisme pemulihan yang lebih aman.
Sumber & bacaan lebih lanjut
- RFC 9110: HTTP Semantics
- Idempotent - MDN Web Docs Glossary
- Idempotency-Key header - MDN Web Docs
- Idempotent requests - Stripe API Reference
– Rio Yotto @rioyotto
