Home / Artikel / Web Development
Web Development

Request API Bisa Terkirim Dua Kali: Cara Mencegah Order dan Pembayaran Ganda

Jaringan yang tidak stabil, tombol yang ditekan berulang, atau retry otomatis dapat membuat satu permintaan API diproses lebih dari sekali. Dengan idempotency key, transaksi database, dan aturan unik yang tepat, aplikas…

Request API Bisa Terkirim Dua Kali: Cara Mencegah Order dan Pembayaran Ganda

Satu klik pada tombol “Bayar” belum tentu hanya menghasilkan satu request. Pengguna bisa menekan tombol dua kali, browser dapat mengirim ulang permintaan setelah koneksi terputus, atau client secara otomatis melakukan retry karena tidak menerima respons tepat waktu.

Masalahnya, server mungkin sebenarnya sudah berhasil membuat pesanan. Hanya saja responsnya hilang di tengah jalan. Ketika client mengirim request yang sama sekali lagi, aplikasi yang tidak dirancang untuk menghadapi kondisi ini bisa membuat dua order, dua invoice, atau bahkan dua transaksi pembayaran.

Di sinilah konsep idempotency penting. Sederhananya, operasi idempotent boleh dijalankan berulang, tetapi hasil akhirnya tetap seperti dijalankan satu kali.

Memahami masalah request ganda

Bayangkan sebuah endpoint berikut:

POST /api/orders

Client mengirim data keranjang dan server membuat order baru. Jika request diterima dua kali, server bisa saja menjalankan proses berikut dua kali:

  1. Membuat nomor order baru.
  2. Menyimpan detail barang.
  3. Mengurangi stok.
  4. Mengirim permintaan pembayaran.
  5. Mengirim email konfirmasi.

Tidak semua langkah tersebut aman untuk diulang. Mengirim email dua kali mungkin mengganggu. Mengurangi stok dua kali bisa membuat jumlah inventaris salah. Namun dampak paling serius biasanya terjadi ketika pembayaran diproses dua kali.

Perlu dibedakan antara request gagal dan hasil request tidak diketahui oleh client. Pada kondisi kedua, client tidak tahu apakah server sudah memproses permintaan. Menganggapnya pasti gagal lalu mengirim ulang adalah sumber banyak duplikasi.

Gunakan idempotency key dari sisi client

Idempotency key adalah nilai unik yang mewakili satu operasi. Client membuat key sebelum mengirim request, lalu menyertakannya di header. Jika request perlu diulang, client harus memakai key yang sama, bukan membuat key baru.

POST /api/orders
Idempotency-Key: 7f8c2a9e-5b31-4b20-9d7e-12a9f1b4c300
Content-Type: application/json

Server kemudian menyimpan key tersebut bersama hasil pemrosesan. Ketika menerima key yang sama, server tidak membuat order baru. Server cukup mengembalikan hasil yang sudah pernah dibuat.

Contoh alurnya:

  1. Client membuat UUID sebagai idempotency key.
  2. Client mengirim request ke server.
  3. Server memeriksa apakah key sudah pernah dipakai.
  4. Jika belum, server memproses transaksi dan menyimpan hasilnya.
  5. Jika sudah, server mengembalikan hasil lama tanpa mengulangi efek samping.

Key tersebut sebaiknya dibuat untuk satu tindakan bisnis, bukan untuk seluruh sesi pengguna. Satu checkout memiliki satu key. Checkout berikutnya harus memakai key yang berbeda.

Simpan key dan hasilnya dalam database

Menyimpan idempotency key hanya di memori aplikasi tidak cukup. Jika aplikasi memiliki beberapa server, request pertama mungkin masuk ke server A, sedangkan retry masuk ke server B. Selain itu, data di memori bisa hilang ketika proses restart.

Buat tabel khusus, misalnya idempotency_requests:

CREATE TABLE idempotency_requests (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT NOT NULL,
    idempotency_key VARCHAR(100) NOT NULL,
    request_hash CHAR(64) NOT NULL,
    response_status INT NOT NULL,
    response_body JSON NOT NULL,
    created_at DATETIME NOT NULL,
    UNIQUE KEY uniq_user_key (user_id, idempotency_key)
);

Kolom request_hash berguna untuk memastikan key yang sama tidak dipakai untuk isi request yang berbeda. Misalnya, key pertama digunakan untuk membeli produk A, tetapi kemudian dikirim lagi dengan isi produk B. Kondisi seperti ini sebaiknya ditolak karena satu key harus mewakili satu operasi yang konsisten.

Jangan lupa menetapkan masa penyimpanan. Key untuk pembayaran mungkin perlu disimpan lebih lama daripada key untuk operasi ringan. Penghapusan data lama dapat dilakukan melalui job terjadwal, tetapi jangan menghapusnya terlalu cepat jika client masih mungkin melakukan retry.

Gabungkan dengan transaksi database

Idempotency key bukan pengganti transaksi database. Keduanya menangani masalah yang berbeda.

  • Idempotency mencegah request yang sama menghasilkan efek ganda.
  • Transaction menjaga agar beberapa perubahan database berhasil bersama-sama atau dibatalkan bersama-sama.
  • Unique constraint menjadi pagar terakhir ketika kode aplikasi mengalami perlombaan request.

Untuk proses membuat order, operasi penting sebaiknya berada dalam transaksi:

BEGIN;

-- Simpan order
-- Simpan detail order
-- Kurangi stok dengan validasi jumlah
-- Simpan catatan idempotency

COMMIT;

Jika stok tidak cukup atau terjadi error, gunakan ROLLBACK. Dengan begitu, aplikasi tidak berada dalam kondisi setengah jadi, misalnya order sudah tercatat tetapi detail barang belum tersimpan.

Namun, transaksi database tidak selalu boleh mencakup panggilan ke layanan eksternal. Menahan transaksi sambil menunggu payment gateway atau layanan email dapat membuat koneksi database terlalu lama terbuka. Untuk proses semacam ini, gunakan status seperti pending, lalu lanjutkan pekerjaan melalui queue atau worker.

Hati-hati dengan efek samping eksternal

Bagian paling sulit adalah efek samping yang terjadi di luar database. Contohnya mengirim email, memanggil payment gateway, atau mengirim webhook ke sistem lain.

Salah satu pola yang bisa digunakan adalah outbox pattern. Aplikasi menyimpan event yang harus dikirim ke tabel outbox dalam transaksi yang sama dengan perubahan order. Worker kemudian membaca tabel tersebut dan mengirim event ke layanan eksternal.

Worker juga harus dirancang agar aman jika menjalankan event yang sama lebih dari sekali. Sistem penerima dapat menggunakan event ID sebagai idempotency key. Dengan cara ini, perlindungan tidak hanya berada di satu sisi.

Jika sebuah operasi memiliki dampak nyata, anggap bahwa request bisa datang dua kali. Jangan menjadikan keberuntungan jaringan sebagai bagian dari desain aplikasi.

Validasi yang perlu dilakukan

Sebelum menerapkan idempotency, lakukan beberapa pemeriksaan praktis:

  • Apakah client memakai key yang sama saat melakukan retry?
  • Apakah key memiliki batas panjang dan format yang jelas?
  • Apakah key terikat pada pengguna atau akun yang benar?
  • Apakah isi request diverifikasi agar tidak berubah untuk key yang sama?
  • Apakah dua request paralel dengan key sama aman terhadap race condition?
  • Apakah respons error juga disimpan atau hanya respons sukses?
  • Apakah operasi eksternal memiliki mekanisme deduplikasi?

Untuk kasus request paralel, jangan hanya melakukan pengecekan “key belum ada” lalu melakukan insert. Dua proses dapat membaca kondisi kosong pada waktu yang hampir bersamaan. Gunakan unique constraint dan tangani error konflik dengan benar.

Apa artinya bagi kita?

Idempotency paling penting untuk endpoint yang membuat atau mengubah sesuatu: checkout, pembayaran, pendaftaran, pembuatan invoice, pengiriman formulir, dan pemesanan tiket. Untuk operasi membaca data seperti GET, masalahnya biasanya berbeda, meskipun cache dan konsistensi tetap perlu diperhatikan.

Mulailah dari satu endpoint yang paling berisiko. Tambahkan idempotency key, tabel penyimpanan hasil, transaksi database, dan pengujian request ganda. Simulasikan koneksi terputus setelah server menyimpan data tetapi sebelum client menerima respons.

Aplikasi yang matang bukan aplikasi yang menganggap jaringan selalu lancar. Aplikasi yang matang adalah aplikasi yang tetap menghasilkan hasil yang benar ketika pengguna mengklik dua kali, koneksi terputus, atau sistem melakukan retry.

– Rio Yotto @rioyotto