Mengatasi Race Condition HTTP Request dengan Atomic Conditional Update
Dalam sistem backend modern—terutama yang berurusan dengan order processing, payment, atau workflow berbasis status—race condition di level HTTP request adalah masalah yang sangat nyata. Kondisi ini sering muncul ketika:
- Client melakukan retry (manual atau otomatis)
- UI mengirim request ganda (double submit)
- Webhook dari pihak ketiga dikirim lebih dari sekali
Jika tidak ditangani dengan benar, race condition dapat menyebabkan:
- Status order berubah berkali-kali
- Proses berat dieksekusi lebih dari satu kali
- Side effect (charge, email, callback) terjadi ganda
Artikel ini membahas salah satu pendekatan paling solid dan praktis untuk mengatasi masalah tersebut: atomic conditional update di level database, dengan memanfaatkan rows affected sebagai indikator idempotency.
Studi Kasus Singkat
Misalkan kita memiliki tabel orders dengan alur status sederhana:
PENDING → PROCESSING → COMPLETED
Beberapa HTTP request bisa datang hampir bersamaan untuk memproses order yang sama:
- Request A dan Request B masuk dalam selisih milidetik
- Keduanya berniat mengubah status dari
PENDINGkePROCESSING
Tanpa mekanisme proteksi, keduanya bisa sama-sama menganggap dirinya request pertama.
Pendekatan yang Digunakan
Solusi yang digunakan adalah:
- Membungkus proses dalam database transaction
- Melakukan conditional UPDATE berdasarkan status saat ini
- Mengecek jumlah baris yang ter-update (
rows affected)
Contoh SQL (disederhanakan):
BEGIN;
UPDATE orders
SET status = 'PROCESSING'
WHERE id = :order_id
AND status = 'PENDING';
-- cek rows affected
COMMIT;
Interpretasi hasil:
rows affected > 0→ request pertama (valid)rows affected = 0→ request kedua atau seterusnya (duplikat)
Kenapa Request Kedua dan Seterusnya Mendapat rows affected = 0?
Ini bagian paling penting untuk dipahami.
Locking Implisit Saat UPDATE
Ketika sebuah UPDATE dijalankan:
- Database akan mengambil lock pada row yang relevan
- Lock ini mencegah perubahan paralel pada row yang sama
Request pertama:
- Menemukan row dengan
status = PENDING - Mengambil lock
- Mengubah status menjadi
PROCESSING - Commit transaction
Apa yang Terjadi pada Request Kedua?
Request kedua datang hampir bersamaan dan mencoba menjalankan query yang sama.
Tergantung timing, ada dua kemungkinan:
a. Request Kedua Datang Saat Lock Masih Aktif
- Request kedua menunggu (block) hingga lock dilepas
- Setelah request pertama
COMMIT, lock dilepas - Request kedua resume eksekusi
Namun pada saat resume:
- Status row sudah berubah menjadi
PROCESSING - Kondisi
WHERE status = 'PENDING'tidak lagi terpenuhi
Akibatnya:
rows affected = 0
b. Request Kedua Datang Setelah Commit
- Tidak ada lock yang perlu ditunggu
- Tapi status sudah
PROCESSING - Kondisi
WHERElangsung gagal
Hasilnya sama:
rows affected = 0
Intinya: Database yang Menjamin Konsistensi
Yang membuat solusi ini kuat adalah:
- Evaluasi kondisi dan update dilakukan secara atomik
- Tidak ada celah antara “cek status” dan “update status”
- Semua race condition dipatahkan di level database
Aplikasi hanya perlu membaca hasil akhirnya.
Ini Bukan Sekadar Pessimistic Locking
Walaupun sering disebut sebagai pessimistic locking, pendekatan ini sebenarnya lebih tepat disebut:
Atomic state transition (compare-and-set)
Perbandingan dengan pendekatan lain:
| Pendekatan | Aman dari Race | Kompleksitas |
|---|---|---|
| Mutex di aplikasi | ❌ | Tinggi |
| Redis / Distributed Lock | ⚠️ | Tinggi |
SELECT ... FOR UPDATE | ✅ | Sedang |
| Optimistic Lock (version) | ✅ | Sedang |
| Conditional UPDATE (ini) | ✅ | Rendah |
Pendekatan ini:
- Lebih simpel
- Lebih efisien
- Lebih mudah dirawat
Kaitan dengan Idempotency
Dengan pola ini:
- Endpoint menjadi idempotent secara alami
- Request duplikat tidak merusak state
- Tidak perlu menyimpan mutex atau state di memory
Selama:
- Status hanya boleh berubah satu arah
- Semua transisi lewat conditional update
Maka sistem akan aman dari duplicate request.
Hal yang Perlu Diperhatikan
Pendekatan ini sangat cocok untuk update status, tapi perlu kehati-hatian jika:
Ada side effect eksternal
- charge payment
- kirim email
- call external API
→ pastikan side effect hanya dijalankan jika
rows affected > 0Workflow kompleks dengan banyak cabang
- pertimbangkan state machine yang eksplisit
Audit dan observability
- log request yang menghasilkan
rows affected = 0
- log request yang menghasilkan
Kesimpulan
Menggunakan conditional UPDATE + rows affected di dalam transaction adalah solusi:
- Benar secara konseptual
- Aman secara concurrency
- Efisien secara performa
- Praktis untuk production
Database digunakan sesuai kekuatannya: sebagai penjaga konsistensi data, bukan sekadar penyimpan.
Untuk banyak kasus idempotency dan race condition di level HTTP request, pendekatan ini bukan hanya cukup—tapi sangat direkomendasikan.