Mengatasi Race Condition HTTP Request dengan Atomic Conditional Update
3 min read

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 PENDING ke PROCESSING

Tanpa mekanisme proteksi, keduanya bisa sama-sama menganggap dirinya request pertama.


Pendekatan yang Digunakan

Solusi yang digunakan adalah:

  1. Membungkus proses dalam database transaction
  2. Melakukan conditional UPDATE berdasarkan status saat ini
  3. 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:

  1. Menemukan row dengan status = PENDING
  2. Mengambil lock
  3. Mengubah status menjadi PROCESSING
  4. 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 WHERE langsung 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:

PendekatanAman dari RaceKompleksitas
Mutex di aplikasiTinggi
Redis / Distributed Lock⚠️Tinggi
SELECT ... FOR UPDATESedang
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:

  1. Ada side effect eksternal

    • charge payment
    • kirim email
    • call external API

    → pastikan side effect hanya dijalankan jika rows affected > 0

  2. Workflow kompleks dengan banyak cabang

    • pertimbangkan state machine yang eksplisit
  3. Audit dan observability

    • log request yang menghasilkan rows affected = 0

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.