Mencegah catatan duplikat di aplikasi CRUD butuh beberapa lapisan: constraint unik di database, kunci idempoten, dan state UI yang mencegah pengiriman ganda.

Catatan duplikat adalah ketika aplikasi Anda menyimpan hal yang sama dua kali. Bisa berupa dua order untuk satu checkout, dua tiket dukungan dengan detail yang sama, atau dua akun yang dibuat dari alur pendaftaran yang sama. Di aplikasi CRUD, duplikat biasanya terlihat seperti baris normal sendiri-sendiri, tetapi salah ketika Anda melihat data secara keseluruhan.
Sebagian besar duplikat bermula dari perilaku normal. Seseorang meng-klik Create dua kali karena halaman terasa lambat. Di ponsel, double tap mudah terlewat. Bahkan pengguna hati-hati akan mencoba lagi jika tombol masih terlihat aktif dan tidak ada tanda yang jelas bahwa sesuatu sedang berlangsung.
Lalu ada bagian tengah yang berantakan: jaringan dan server. Sebuah request bisa time out dan di-retry otomatis. Library klien mungkin mengulangi POST jika mengira usaha pertama gagal. Request pertama mungkin berhasil, tetapi respons hilang, sehingga pengguna mencoba lagi dan membuat salinan kedua.
Anda tidak bisa menyelesaikan ini hanya di satu lapisan karena tiap lapisan hanya melihat sebagian cerita. UI bisa mengurangi pengiriman ganda yang tidak disengaja, tetapi tidak bisa menghentikan retry dari koneksi buruk. Server bisa mendeteksi pengulangan, tetapi butuh cara andal untuk mengenali “ini create yang sama lagi.” Database bisa menegakkan aturan, tetapi hanya jika Anda mendefinisikan apa yang dimaksud dengan “hal yang sama.”
Tujuannya sederhana: buat create aman meskipun permintaan yang sama terjadi dua kali. Upaya kedua harus menjadi no-op, respons bersih “sudah dibuat”, atau konflik yang terkendali, bukan baris kedua.
Banyak tim memperlakukan duplikat sebagai masalah database. Dalam praktiknya, duplikat biasanya lahir lebih awal, ketika aksi create yang sama dipicu lebih dari sekali.
Pengguna meng-klik Create dan tidak terlihat apa-apa, jadi mereka meng-klik lagi. Atau mereka menekan Enter, lalu meng-klik tombol sesudahnya. Di mobile, Anda bisa mendapat dua ketukan cepat, event touch dan click yang tumpang tindih, atau gesture yang terdaftar dua kali.
Bahkan jika pengguna hanya submit sekali, jaringan masih bisa mengulangi request. Timeout dapat memicu retry. Aplikasi offline mungkin mengantri “Save” dan mengirimkannya kembali saat terhubung. Beberapa library HTTP mencoba ulang otomatis pada error tertentu, dan Anda tidak akan menyadari sampai melihat baris duplikat.
Server sengaja mengulangi pekerjaan. Antrian job me-retry job yang gagal. Penyedia webhook sering mengirim event yang sama lebih dari sekali, terutama jika endpoint Anda lambat atau mengembalikan status non-2xx. Jika logika create Anda dipicu oleh event ini, anggaplah duplikat akan terjadi.
Konkruensi menghasilkan duplikat yang paling licik. Dua tab submit form yang sama dalam hitungan milidetik. Jika server Anda melakukan “apakah ini sudah ada?” lalu insert, kedua request bisa lolos pemeriksaan sebelum salah satu insert terjadi.
Perlakukan klien, jaringan, dan server sebagai sumber pengulangan yang terpisah. Anda akan membutuhkan pertahanan di ketiganya.
Jika Anda ingin satu tempat yang andal untuk menghentikan duplikat, letakkan aturannya di database. Perbaikan UI dan pemeriksaan server membantu, tetapi mereka bisa gagal saat retry, lag, atau dua pengguna bertindak bersamaan. Constraint unik di database adalah otoritas terakhir.
Mulailah dengan memilih aturan keunikan dunia nyata yang sesuai dengan cara orang memikirkan record. Contoh umum:
Hati-hati dengan field yang tampak unik tapi bukan, seperti nama lengkap.
Setelah Anda punya aturan, tegakkan dengan unique constraint (atau indeks unik). Ini membuat database menolak insert kedua yang akan melanggar aturan, bahkan jika dua request tiba pada saat yang sama.
Saat constraint memicu, putuskan pengalaman yang dilihat pengguna. Jika membuat duplikat selalu salah, blokir dengan pesan jelas ("Alamat email itu sudah digunakan"). Jika retry sering terjadi dan record sudah ada, seringkali lebih baik menganggap retry sebagai sukses dan mengembalikan record yang ada ("Order Anda sudah dibuat").
Jika create Anda memang "create atau reuse," upsert bisa menjadi pola yang paling bersih. Contoh: "buat customer berdasarkan email" dapat memasukkan baris baru atau mengembalikan yang sudah ada. Gunakan ini hanya jika sesuai makna bisnis. Jika payload sedikit berbeda bisa datang untuk kunci yang sama, putuskan field mana yang boleh diupdate dan mana yang harus tetap tidak berubah.
Constraint unik tidak menggantikan kunci idempoten atau state UI yang baik, tetapi memberi Anda stop keras yang dapat diandalkan oleh semua lapisan lain.
Kunci idempoten adalah token unik yang mewakili satu intent pengguna, seperti "buat order ini sekali." Jika request yang sama dikirim lagi (klik ganda, retry jaringan, resume mobile), server memperlakukannya sebagai retry, bukan create baru.
Ini adalah salah satu alat paling praktis untuk membuat endpoint create aman ketika klien tidak tahu apakah usaha pertama berhasil.
Endpoint yang paling diuntungkan adalah yang duplikatnya mahal atau membingungkan, seperti order, invoice, pembayaran, undangan, langganan, dan form yang memicu email atau webhook.
Pada retry, server harus mengembalikan hasil asli dari percobaan pertama yang berhasil, termasuk ID record yang dibuat dan kode status yang sama. Untuk melakukan itu, simpan sebuah catatan idempoten kecil yang dipetakan oleh (user atau account) + endpoint + kunci idempoten. Simpan juga hasilnya (ID record, body respons) dan status "sedang diproses" sehingga dua request yang hampir bersamaan tidak membuat dua baris.
Simpan catatan idempoten cukup lama untuk menutup retry nyata. Baseline umum adalah 24 jam. Untuk pembayaran, banyak tim menyimpan 48–72 jam. TTL menjaga penyimpanan tetap terbatas dan sesuai berapa lama retry mungkin terjadi.
Jika Anda menghasilkan API dengan builder yang digerakkan chat seperti Koder.ai, Anda tetap ingin membuat idempoten eksplisit: terima kunci yang dikirim klien (header atau field) dan tegakkan "kunci sama, hasil sama" di server.
Idempoten membuat request create aman untuk diulang. Jika klien retry karena timeout (atau pengguna klik dua kali), server mengembalikan hasil yang sama alih-alih membuat baris kedua.
Idempotency-Key), tetapi mengirimkannya di body JSON juga bisa.Detail kuncinya adalah bahwa "cek + simpan" harus aman terhadap konkurensi. Dalam praktiknya, Anda menyimpan catatan idempoten dengan unique constraint pada (scope, key) dan memperlakukan konflik sebagai sinyal untuk menggunakan kembali.
if idempotency_record exists for (user_id, key):
return saved_response
else:
begin transaction
create the row
save response under (user_id, key)
commit
return response
Contoh: seorang customer menekan "Create invoice," aplikasi mengirim kunci abc123, dan server membuat invoice inv_1007. Jika telepon kehilangan sinyal dan me-retry, server menjawab dengan respons yang sama inv_1007, bukan inv_1008.
Saat Anda menguji, jangan berhenti pada "klik dua kali." Simulasikan request yang time out di klien tetapi tetap selesai di server, lalu retry dengan kunci yang sama.
Pertahanan sisi server penting, tetapi banyak duplikat masih dimulai dari manusia yang melakukan hal normal dua kali. UI yang baik membuat jalur aman menjadi jelas.
Nonaktifkan tombol submit segera setelah pengguna mengirim. Lakukan pada klik pertama, bukan setelah validasi atau setelah request dimulai. Jika form bisa submit lewat beberapa kontrol (tombol dan Enter), kunci seluruh state form, bukan hanya satu tombol.
Tampilkan state progres yang jelas yang menjawab satu pertanyaan: apakah sedang berlangsung? Label sederhana "Menyimpan..." atau spinner sudah cukup. Pertahankan tata letak agar stabil sehingga tombol tidak bergeser dan menggoda klik kedua.
Sekumpulan aturan kecil mencegah sebagian besar pengiriman ganda: set flag isSubmitting di awal handler submit, abaikan submit baru saat flag true (untuk klik dan Enter), dan jangan hapus sampai Anda mendapatkan respons nyata.
Respons yang lambat adalah tempat banyak aplikasi tergelincir. Jika Anda mengaktifkan kembali tombol berdasarkan timer tetap (mis. setelah 2 detik), pengguna bisa submit lagi sementara percobaan pertama masih berjalan. Aktifkan kembali hanya saat percobaan selesai.
Setelah sukses, buat resubmission menjadi tidak mungkin. Navigasikan pergi (ke halaman record baru atau daftar) atau tunjukkan state sukses yang jelas dengan record yang dibuat terlihat. Hindari meninggalkan form yang terisi di layar dengan tombol yang aktif.
Bug duplikat yang bandel datang dari perilaku sehari-hari yang "aneh tapi umum": dua tab, refresh, atau ponsel yang kehilangan sinyal.
Pertama, scopes keunikan dengan benar. "Unik" jarang berarti "unik di seluruh database." Mungkin berarti satu per pengguna, satu per workspace, atau satu per tenant. Jika Anda sinkron dengan sistem eksternal, Anda mungkin perlu keunikan per sumber eksternal plus ID eksternalnya. Pendekatan aman adalah menuliskan kalimat persis yang Anda maksud (mis. "Satu nomor invoice per tenant per tahun"), lalu tegakkan itu.
Perilaku multi-tab adalah jebakan klasik. State pemuatan UI membantu di satu tab, tetapi tidak melakukan apa-apa lintas tab. Di sinilah pertahanan sisi server harus tetap kuat.
Tombol kembali dan refresh bisa memicu resubmit tidak sengaja. Setelah create sukses, pengguna sering refresh untuk “memeriksa,” atau menekan Back dan mengirim ulang form yang masih terlihat bisa diedit. Lebih baik menampilkan tampilan created daripada form asli, dan buat server menangani replay yang aman.
Mobile menambah gangguan: backgrounding, jaringan fluktuatif, dan retry otomatis. Sebuah request mungkin berhasil, tetapi aplikasi tidak menerima respons, sehingga mencoba lagi saat dilanjutkan.
Mode kegagalan yang paling umum adalah menganggap UI sebagai satu-satunya pembatas. Tombol dinonaktifkan dan spinner membantu, tetapi mereka tidak menutup refresh, jaringan mobile yang fluktuatif, pengguna membuka tab kedua, atau bug klien. Server dan database masih harus bisa mengatakan "create ini sudah terjadi."
Perangkap lain adalah memilih field yang salah untuk keunikan. Jika Anda menetapkan unique constraint pada sesuatu yang tidak benar-benar unik (nama belakang, timestamp yang dibulatkan, judul bebas), Anda akan memblokir record yang valid. Sebagai gantinya, gunakan identifier nyata (seperti ID provider eksternal) atau aturan berskala (unik per pengguna, per hari, atau per record induk).
Kunci idempoten juga mudah diimplementasikan dengan buruk. Jika klien menghasilkan kunci baru setiap retry, Anda mendapatkan create baru setiap kali. Gunakan kunci yang sama untuk seluruh intent pengguna, dari klik pertama sampai semua retry.
Perhatikan juga apa yang Anda kembalikan saat retry. Jika request pertama membuat record, retry harus mengembalikan hasil yang sama (atau setidaknya ID record yang sama), bukan error samar yang membuat pengguna mencoba lagi.
Jika constraint unik memblokir duplikat, jangan sembunyikan di balik "Terjadi kesalahan." Katakan apa yang terjadi dengan bahasa sederhana: "Nomor invoice ini sudah ada. Kami menyimpan yang asli dan tidak membuat yang kedua."
Sebelum rilis, lakukan pengecekan cepat khusus untuk jalur pembuatan duplikat. Hasil terbaik datang dari menumpuk pertahanan sehingga klik terlewat, retry, atau jaringan lambat tidak bisa membuat dua baris.
Konfirmasi tiga hal:
Cek praktis: buka form, klik submit dua kali cepat, lalu refresh di tengah submit dan coba lagi. Jika Anda bisa membuat dua record, pengguna nyata juga akan bisa.
Bayangkan aplikasi invoicing kecil. Seorang pengguna mengisi invoice baru dan menekan Create. Jaringan lambat, layar tidak segera berubah, dan mereka menekan Create lagi.
Dengan proteksi UI saja, Anda mungkin menonaktifkan tombol dan menampilkan spinner. Itu membantu, tetapi tidak cukup. Double tap masih bisa lolos di beberapa perangkat, retry bisa terjadi setelah timeout, atau pengguna bisa submit dari dua tab.
Dengan hanya constraint unik di database, Anda bisa menghentikan duplikat yang sama persis, tetapi pengalamannya bisa kasar. Request pertama berhasil, request kedua terkena constraint, dan pengguna melihat error padahal invoice sudah dibuat.
Hasil bersih adalah idempoten plus constraint unik:
Pesan UI sederhana setelah ketukan kedua: "Invoice dibuat - kami mengabaikan pengiriman duplikat dan mempertahankan permintaan pertama Anda."
Setelah baseline terpasang, kemenangan selanjutnya soal visibilitas, pembersihan, dan konsistensi.
Tambahkan logging ringan di jalur create sehingga Anda bisa membedakan antara aksi pengguna nyata dan retry. Log kunci idempoten, field unik yang terlibat, dan hasilnya (created vs returned existing vs rejected). Anda tidak perlu tooling berat untuk memulai.
Jika duplikat sudah ada, bersihkan dengan aturan yang jelas dan jejak audit. Misalnya, pertahankan record tertua sebagai “pemenang,” pasang kembali baris terkait (pembayaran, item baris), dan tandai yang lain sebagai digabungkan daripada menghapus. Itu memudahkan dukungan dan pelaporan.
Tuliskan aturan keunikan dan idempoten Anda di satu tempat: apa yang unik dan dalam scope apa, berapa lama kunci idempoten hidup, seperti apa error terlihat, dan apa yang harus dilakukan UI pada retry. Ini mencegah endpoint baru diam-diam melewati rel keselamatan.
Jika Anda membangun layar CRUD dengan cepat di Koder.ai (koder.ai), ada baiknya membuat perilaku ini bagian dari template default Anda: constraint unik di skema, endpoint create yang idempoten di API, dan state loading yang jelas di UI. Dengan begitu, kecepatan tidak mengorbankan data yang berantakan.
Sebuah catatan duplikat terjadi ketika hal dunia nyata yang sama tersimpan dua kali, misalnya dua pesanan untuk satu checkout atau dua tiket untuk masalah yang sama. Biasanya ini terjadi karena aksi “create” yang sama dijalankan lebih dari sekali akibat klik ganda pengguna, retry, atau permintaan yang bersamaan.
Karena aksi create kedua bisa dipicu tanpa disadari pengguna, seperti double tap di mobile atau menekan Enter lalu meng-klik tombol. Bahkan jika pengguna hanya submit sekali, klien, jaringan, atau server dapat me-retry request setelah timeout, dan server tidak bisa menganggap bahwa "POST berarti sekali saja."
Tidak bisa diandalkan. Menonaktifkan tombol dan menampilkan “Menyimpan…” mengurangi klik ganda tidak sengaja, tetapi tidak menghentikan retry dari jaringan yang tidak stabil, refresh, multi-tab, worker background, atau pengiriman ulang webhook. Anda tetap memerlukan pertahanan di server dan database.
Constraint unik adalah garis pertahanan terakhir yang menghentikan dua baris dimasukkan sekaligus bahkan jika dua request tiba bersamaan. Ini paling efektif bila Anda mendefinisikan aturan keunikan dunia nyata (seringkali berskala, mis. per tenant atau workspace) dan menegakkannya langsung di database.
Keduanya menyelesaikan masalah yang berbeda. Constraint unik memblokir duplikat berdasarkan aturan field (mis. nomor invoice), sementara kunci idempoten membuat satu percobaan create aman diulang (kunci yang sama mengembalikan hasil yang sama). Menggunakan keduanya memberi Anda keamanan plus pengalaman pengguna yang lebih baik pada retry.
Hasilkan satu kunci per intent pengguna (satu kali tekan “Create”), gunakan kembali untuk semua retry intent itu, dan kirimkan bersama request setiap kali. Kunci harus stabil lewat timeout dan saat aplikasi dilanjutkan, namun tidak boleh digunakan lagi untuk create berbeda di kemudian hari.
Simpan sebuah catatan idempoten yang dikunci oleh scope (mis. user atau account), endpoint, dan kunci idempoten tersebut, lalu simpan respons yang Anda kembalikan pada permintaan pertama yang sukses. Jika kunci yang sama datang lagi, kembalikan respons yang disimpan dengan ID record yang sama daripada membuat baris baru.
Gunakan pendekatan “cek + simpan” yang aman terhadap concurrency, biasanya dengan menegakkan constraint unik pada catatan idempoten itu sendiri (untuk scope dan kunci). Dengan begitu, dua request hampir bersamaan tidak bisa sama-sama menganggap diri mereka yang “pertama,” dan salah satunya akan dipaksa menggunakan kembali hasil yang tersimpan.
Simpan cukup lama untuk menutup retry yang realistis; default umum adalah sekitar 24 jam, lebih lama untuk alur seperti pembayaran di mana retry mungkin terjadi kemudian. Tambahkan TTL agar penyimpanan tidak tumbuh terus, dan pastikan TTL cocok dengan berapa lama klien kemungkinan melakukan retry.
Anggap retry sebagai keberhasilan ketika memang niatnya sama, dan kembalikan record asli (ID yang sama) daripada error samar. Jika pengguna memang mencoba membuat sesuatu yang harus unik (mis. email), kembalikan pesan konflik yang jelas yang menjelaskan apa yang sudah ada dan apa yang dilakukan sistem selanjutnya.