Race condition di aplikasi CRUD bisa menyebabkan pesanan ganda dan total yang salah. Pelajari titik tabrakan umum serta perbaikan praktis dengan konstraint, kunci, dan penjagaan UX.

Race condition terjadi ketika dua (atau lebih) permintaan memperbarui data yang sama hampir bersamaan, dan hasil akhir bergantung pada urutan waktu. Setiap permintaan terlihat benar sendiri. Bersama-sama, mereka menghasilkan hasil yang salah.
Contoh sederhana: dua orang menekan Simpan pada rekaman pelanggan yang sama dalam waktu satu detik. Satu memperbarui email, yang lain memperbarui nomor telepon. Jika kedua permintaan mengirim seluruh rekaman, tulis kedua bisa saling menimpa; perubahan salah satu hilang tanpa error.
Fenomena ini lebih sering terlihat pada aplikasi cepat karena pengguna dapat memicu lebih banyak aksi per menit. Ini juga melonjak pada momen sibuk: flash sale, akhir bulan laporan, kampanye email besar, atau kapan pun backlog permintaan mengenai baris yang sama.
Pengguna jarang melaporkan "race condition." Mereka melaporkan gejalanya: pesanan atau komentar ganda, pembaruan yang hilang ("saya sudah menyimpan, tapi kembali seperti semula"), total yang aneh (inventori jadi negatif, penghitung mundur), atau status yang berubah tak terduga (disetujui, lalu kembali menunggu).
Retry memperparahnya. Orang klik dua kali, refresh setelah respons lambat, submit dari dua tab, atau menghadapi jaringan fluktuatif yang menyebabkan browser dan aplikasi mobile mengirim ulang. Jika server memperlakukan setiap permintaan sebagai tulis baru, Anda bisa mendapatkan dua create, dua pembayaran, atau dua perubahan status yang seharusnya terjadi sekali.
Kebanyakan aplikasi CRUD terasa sederhana: baca baris, ubah field, simpan. Masalahnya, aplikasi Anda tidak mengendalikan waktu. Database, jaringan, retry, pekerjaan latar, dan perilaku pengguna semuanya tumpang tindih.
Salah satu pemicu umum adalah dua orang mengedit rekaman yang sama. Keduanya memuat nilai "saat ini", keduanya membuat perubahan yang valid, dan simpanan terakhir diam-diam menimpa yang pertama. Tidak ada yang salah secara eksplisit, tapi satu pembaruan hilang.
Ini juga terjadi pada satu orang. Klik ganda pada tombol Simpan, mengetuk kembali dan maju, atau koneksi lambat yang mendorong seseorang menekan Submit lagi bisa mengirim tulis yang sama dua kali. Jika endpoint tidak idempoten, Anda bisa membuat duplikat, menagih dua kali, atau memajukan status dua langkah.
Kebiasaan modern menambah tumpang tindih. Banyak tab atau perangkat yang masuk ke akun yang sama dapat memicu pembaruan saling bertentangan. Pekerjaan latar (email, penagihan, sinkron, pembersihan) bisa menyentuh baris yang sama dengan permintaan web. Retry otomatis di klien, load balancer, atau job runner bisa mengulangi permintaan yang sebenarnya sudah berhasil.
Jika Anda cepat merilis fitur, rekaman yang sama sering diperbarui dari lebih banyak tempat daripada yang diingat siapa pun. Jika Anda menggunakan builder chat seperti Koder.ai, aplikasi bisa tumbuh lebih cepat lagi, jadi anggap konkurensi sebagai perilaku normal, bukan kasus tepi.
Race condition jarang muncul di demo "buat record". Mereka muncul di tempat dua permintaan menyentuh potongan kebenaran yang sama pada waktu hampir bersamaan. Mengetahui hotspot biasa membantu Anda merancang penulisan yang aman sejak awal.
Apa pun yang terasa seperti "tinggal tambah 1" bisa rusak saat beban tinggi: like, view count, total, nomor faktur, nomor tiket. Pola berisiko adalah membaca nilai, menambah, lalu menulis kembali. Dua permintaan bisa membaca nilai awal yang sama dan menimpa satu sama lain.
Alur seperti Draft -> Submitted -> Approved -> Paid terlihat sederhana, tapi tabrakan umum terjadi. Masalah muncul ketika dua aksi memungkinkan pada saat yang sama (approve dan edit, cancel dan pay). Tanpa penjagaan, Anda bisa mendapatkan rekaman yang melewatkan langkah, berbalik, atau menampilkan status berbeda di tabel yang berbeda.
Anggap perubahan status seperti kontrak: izinkan hanya langkah valid berikutnya dan tolak yang lain.
Sisa kursi, jumlah stok, slot janji, dan field "kapasitas tersisa" menciptakan masalah oversell klasik. Dua pembeli checkout bersamaan, keduanya melihat ketersediaan, dan keduanya berhasil. Jika database bukan hakim akhir, lama-lama Anda akan menjual lebih dari stok.
Beberapa aturan bersifat absolut: satu email per akun, satu langganan aktif per pengguna, satu keranjang terbuka per pengguna. Ini sering gagal ketika Anda cek dulu ("apakah sudah ada?") lalu insert. Di bawah konkurensi, kedua permintaan bisa lolos pengecekan.
Jika Anda cepat menulis alur CRUD (misalnya, dengan chat ke app di Koder.ai), catat hotspot ini sejak awal dan dukung mereka dengan konstraint dan penulisan aman, bukan hanya pengecekan UI.
Banyak race condition dimulai dari sesuatu yang membosankan: aksi yang sama dikirim dua kali. Pengguna klik dua kali. Jaringan lambat sehingga mereka klik lagi. Ponsel merekam dua ketukan. Kadang tidak disengaja: halaman melakukan refresh setelah POST dan browser menawarkan untuk submit ulang.
Saat itu terjadi, backend bisa menjalankan dua create atau update secara paralel. Jika keduanya sukses, Anda mendapatkan duplikat, total yang salah, atau perubahan status yang berjalan dua kali (misalnya, approve kemudian approve lagi). Terlihat acak karena bergantung pada waktu.
Pendekatan paling aman adalah pertahanan berlapis. Perbaiki UI, tapi anggap UI akan gagal.
Perubahan praktis yang bisa diterapkan pada sebagian besar alur tulis:
Contoh: pengguna mengetuk "Bayar invoice" dua kali di mobile. UI harus memblokir ketukan kedua. Server juga harus menolak permintaan kedua ketika melihat kunci idempoten yang sama, mengembalikan hasil sukses asli alih-alih menagih lagi.
Field status terasa sederhana sampai dua hal mencoba mengubahnya bersamaan. Pengguna klik Approve sementara job otomatis menandai rekaman yang sama Expired, atau dua anggota tim mengerjakan item yang sama di tab berbeda. Kedua pembaruan bisa sukses, tapi status akhir bergantung pada waktu, bukan aturan Anda.
Anggap status sebagai mesin status kecil. Simpan tabel singkat langkah yang diizinkan (mis. Draft -> Submitted -> Approved, dan Submitted -> Rejected). Lalu setiap tulis memeriksa: "Apakah langkah ini diizinkan dari status saat ini?" Jika tidak, tolak daripada menimpa diam-diam.
Optimistic locking membantu menangkap pembaruan usang tanpa memblokir pengguna lain. Tambahkan nomor versi (atau updated_at) dan minta kecocokan saat menyimpan. Jika orang lain mengubah baris setelah Anda memuatnya, pembaruan Anda mempengaruhi nol baris dan Anda bisa menampilkan pesan jelas seperti "Item ini berubah, refresh dan coba lagi."
Polanya sederhana untuk update status:
Juga, tempatkan perubahan status di satu tempat. Jika update tersebar di banyak layar, job latar, dan webhook, Anda akan melewatkan aturan. Letakkan di balik satu fungsi atau endpoint yang menegakkan pengecekan transisi yang sama setiap kali.
Bug counter paling umum terlihat sepele: aplikasi membaca nilai, menambah 1, lalu menulis kembali. Saat beban tinggi, dua permintaan bisa membaca angka yang sama dan keduanya menulis nilai baru yang sama, sehingga satu kenaikan hilang. Sulit dideteksi karena "biasanya bekerja" saat testing.
Jika sebuah nilai hanya di-increment atau di-decrement, biarkan database yang melakukannya dalam satu pernyataan. Maka database menerapkan perubahan dengan aman meskipun banyak permintaan datang bersamaan.
UPDATE posts
SET like_count = like_count + 1
WHERE id = $1;
Ide yang sama berlaku untuk inventori, view count, retry counter, dan apa pun yang bisa dinyatakan sebagai "baru = lama + delta".
Total sering salah ketika Anda menyimpan angka turunan (order_total, account_balance, project_hours) lalu memperbaruinya dari banyak tempat. Jika Anda bisa menghitung total dari baris sumber (line items, entri buku besar), Anda menghindari kelas bug drift.
Saat Anda harus menyimpan total demi kecepatan, anggap itu sebagai tulis kritis. Gabungkan update baris sumber dan total yang disimpan dalam satu transaksi. Pastikan hanya satu penulis yang bisa memperbarui total yang sama sekaligus (locking, guarded updates, atau path pemilik tunggal). Tambahkan konstraint yang mencegah nilai mustahil (misalnya inventori negatif). Lalu rekonsiliasi sesekali dengan pengecekan latar yang menghitung ulang dan menandai ketidaksesuaian.
Contoh konkret: dua pengguna menambahkan item ke keranjang yang sama bersamaan. Jika setiap permintaan membaca cart_total, menambah harga itemnya, dan menulis kembali, satu penambahan bisa hilang. Jika Anda memperbarui item keranjang dan total keranjang bersama-sama dalam satu transaksi, total tetap benar meskipun ada klik paralel.
Jika Anda ingin lebih sedikit race condition, mulai dari database. Kode aplikasi bisa retry, timeout, atau berjalan dua kali. Konstraint database adalah gerbang akhir yang tetap benar bahkan saat dua permintaan datang bersamaan.
Konstraint unik menghentikan duplikat yang "seharusnya tak pernah terjadi" tapi terjadi: alamat email, nomor pesanan, ID faktur, atau aturan "satu langganan aktif per pengguna". Ketika dua signup mendarat bersamaan, database menerima satu baris dan menolak yang lain.
Foreign keys mencegah referensi rusak. Tanpa mereka, satu permintaan bisa menghapus record induk sementara permintaan lain membuat record anak yang merujuk pada tidak ada apa-apa, meninggalkan row yatim yang sulit dibersihkan.
Check constraints menjaga nilai dalam rentang aman dan menegakkan aturan status sederhana. Misalnya quantity >= 0, rating antara 1 dan 5, atau status terbatas ke set yang diizinkan.
Anggap kegagalan konstraint sebagai hasil yang diharapkan, bukan "error server." Tangkap pelanggaran unik, foreign key, dan check, kembalikan pesan jelas seperti "Email itu sudah digunakan," dan log detail untuk debugging tanpa membocorkan internals.
Contoh: dua orang menekan "Buat order" dua kali saat lag. Dengan konstraint unik pada (user_id, cart_id), Anda tidak mendapatkan dua order. Anda mendapatkan satu order dan satu penolakan yang bersih dan bisa dijelaskan.
Beberapa tulis bukan satu pernyataan. Anda baca baris, cek aturan, update status, dan mungkin insert log audit. Jika dua permintaan melakukan itu bersamaan, keduanya bisa lulus cek dan menulis. Itu pola kegagalan klasik.
Bungkus penulisan multi-langkah dalam satu transaksi sehingga semua langkah sukses bersama atau tidak sama sekali. Lebih penting lagi, transaksi memberi Anda tempat untuk mengontrol siapa yang boleh mengubah data yang sama pada saat yang sama.
Saat hanya satu aktor bisa mengedit record pada satu waktu, gunakan row-level lock. Misalnya: lock baris order, konfirmasi masih dalam status "pending", lalu ubah ke "approved" dan tulis entry audit. Permintaan kedua akan menunggu, lalu memeriksa ulang status dan berhenti.
Pilih berdasarkan seberapa sering tabrakan terjadi:
Jaga waktu lock tetap singkat. Lakukan pekerjaan sesedikit mungkin saat mengunci: jangan panggil API eksternal, jangan lakukan kerja file yang lambat, jangan loop besar. Jika Anda membangun alur di tool seperti Koder.ai, buat transaksi hanya untuk langkah database, lalu lakukan sisanya setelah commit.
Pilih satu alur yang bisa membuat rugi uang atau kepercayaan saat bertabrakan. Yang umum: buat order, reservasi stok, lalu set status order jadi confirmed.
Tulis langkah persis yang kode Anda lakukan sekarang, berurutan. Spesifik tentang apa yang dibaca, apa yang ditulis, dan apa arti "sukses." Tabrakan bersembunyi di celah antara baca dan tulis yang terlambat.
Jalur penguatan yang bekerja di sebagian besar stack:
Tambahkan satu test yang membuktikan perbaikan. Jalankan dua permintaan bersamaan terhadap produk dan kuantitas yang sama. Pastikan tepat satu order terkonfirmasi, dan yang lain gagal dengan cara terkontrol (tidak ada stok negatif, tidak ada baris reservasi duplikat).
Jika Anda menghasilkan aplikasi dengan cepat (termasuk menggunakan platform seperti Koder.ai), checklist ini tetap layak diterapkan pada beberapa jalur tulis yang paling penting.
Salah satu penyebab terbesar race condition adalah mempercayai UI. Tombol yang dinonaktifkan dan cek sisi-klien membantu, tapi pengguna bisa klik dua kali, refresh, buka dua tab, atau memutar ulang permintaan dari koneksi yang fluktuatif. Jika server tidak idempoten, duplikat lolos.
Bug diam lain: Anda menangkap error database (seperti pelanggaran konstraint unik) tapi meneruskan alur seolah-olah berhasil. Itu sering berubah jadi "pembuatan gagal, tapi kami tetap mengirim email" atau "pembayaran gagal, tapi kami tetap menandai order terbayar." Setelah efek samping terjadi, sulit mundur.
Transaksi panjang juga perangkap. Jika Anda membuka transaksi sambil memanggil email, pembayaran, atau API pihak ketiga, Anda menahan lock lebih lama dari perlu. Itu meningkatkan waktu tunggu, timeouts, dan kemungkinan permintaan saling menunggu.
Mencampur pekerjaan latar dan aksi pengguna tanpa satu sumber kebenaran menciptakan keadaan split-brain. Job retry mengupdate baris sementara pengguna sedang mengeditnya, dan sekarang keduanya mengira mereka penulis terakhir.
Beberapa "perbaikan" yang sebenarnya tidak memperbaiki:
Jika Anda membangun dengan alat chat-to-app seperti Koder.ai, aturan yang sama berlaku: mintalah konstraint sisi-server dan batasan transaksi yang jelas, bukan hanya penjagaan UI yang lebih rapi.
Race condition sering muncul hanya di lalu lintas nyata. Pemeriksan sebelum rilis bisa menangkap titik tabrakan paling umum tanpa rewrite besar.
Mulailah dengan database. Jika sesuatu harus unik (email, nomor faktur, satu langganan aktif per pengguna), jadikan itu konstraint unik nyata, bukan aturan cek di aplikasi. Lalu pastikan kode Anda mengantisipasi konstraint bisa gagal dan mengembalikan respons yang jelas dan aman.
Selanjutnya, lihat status. Setiap perubahan status (Draft -> Submitted -> Approved) harus divalidasi terhadap set transisi yang eksplisit. Jika dua permintaan mencoba memindahkan rekaman yang sama, yang kedua harus ditolak atau jadi no-op, bukan menciptakan keadaan di antaranya.
Checklist praktis pra-rilis:
Jika Anda membangun alur di Koder.ai, anggap ini sebagai acceptance criteria: aplikasi yang dihasilkan harus gagal dengan aman saat diulang dan dalam konkurensi, bukan hanya lewat jalur bahagia.
Dua staf membuka permintaan pembelian yang sama. Keduanya menekan Approve dalam beberapa detik. Kedua permintaan mencapai server.
Apa yang bisa salah berantakan: permintaan di-"approve" dua kali, dua notifikasi dikirim, dan total yang terkait approval (anggaran terpakai, jumlah approval harian) bisa naik dua. Kedua update valid sendiri, tapi mereka bertabrakan.
Berikut rencana perbaikan yang bekerja baik dengan database gaya PostgreSQL.
Tambahkan aturan yang menjamin hanya satu record approval dapat ada untuk sebuah request. Misalnya, simpan approval di tabel terpisah dan terapkan UNIQUE constraint pada request_id. Sekarang insert kedua akan gagal meskipun kode aplikasi bermasalah.
Saat approve, lakukan seluruh transisi dalam satu transaksi:
Jika staf kedua datang terlambat, mereka akan melihat 0 row terupdate atau error konstraint unik. Dalam kedua kasus, hanya satu perubahan yang menang.
Setelah perbaikan, staf pertama melihat Approved dan mendapat konfirmasi biasa. Staf kedua melihat pesan ramah seperti: "Permintaan ini sudah disetujui oleh orang lain. Refresh untuk melihat status terbaru." Tidak ada loading tanpa akhir, tidak ada notifikasi ganda, tidak ada kegagalan diam.
Jika Anda menghasilkan flow CRUD di platform seperti Koder.ai (backend Go dengan PostgreSQL), Anda bisa menanamkan pengecekan ini ke dalam action approve sekali lalu pakai ulang pola untuk aksi "hanya satu pemenang" lainnya.
Race condition paling mudah diperbaiki bila Anda memperlakukannya sebagai rutinitas yang bisa diulang, bukan perburuan bug sekali-sekali. Fokus pada beberapa jalur tulis yang paling penting, dan buat mereka benar secara membosankan sebelum memoles hal lain.
Mulai dengan menamai titik tabrakan teratas Anda. Di banyak aplikasi CRUD itu trio yang sama: counter (like, inventori, saldo), perubahan status (Draft -> Submitted -> Approved), dan pengiriman ganda (klik dua kali, retry, jaringan lambat).
Rutinitas yang tahan lama:
Jika Anda membangun di Koder.ai, Planning Mode adalah tempat praktis untuk memetakan setiap alur tulis sebagai langkah dan aturan sebelum menghasilkan perubahan di Go dan PostgreSQL. Snapshot dan rollback juga berguna saat Anda mengirim konstraint baru atau perilaku lock dan ingin cara cepat kembali bila menemukan edge case.
Seiring waktu, ini menjadi kebiasaan: setiap fitur tulis baru mendapat konstraint, rencana transaksi, dan test konkurensi. Begitulah race condition di aplikasi CRUD berhenti menjadi kejutan.