Back to Blog
Feb 3, 20265 min read

Studi Kasus Golang: Bagaimana Saya Menyelamatkan Saldo User dari Race Condition (1.000 Transaksi/Detik)

Tirta Afandi
Tirta Afandi
@kikuk_afandi

Niat Awal Cuma Belajar

Jujur saja, proyek ini bermula dari rasa "kangen" ngoding. Setelah sempat vakum belajar Go (Golang) karena kesibukan, saya memutuskan untuk kembali deep dive. Tapi kali ini, saya tidak mau cuma bikin "To-Do List App" yang klise. Saya ingin tantangan nyata.

Saya memutuskan membangun PayAja, sebuah simulasi backend E-Wallet (Dompet Digital). Targetnya bukan cuma "jalan", tapi harus scalable dan maintainable. Makanya, saya langsung menerapkan standar industri: Arsitektur Microservices dengan pendekatan Clean Architecture (Domain-Driven Design).

Awalnya, semua terasa mulus. Struktur folder rapi, dependency injection berjalan cantik, dan komunikasi gRPC antar service sangat cepat. Saya merasa, "Wah, sudah siap deploy nih."

Sampai akhirnya, saya melakukan hal iseng yang mengubah segalanya: Stress Test.


Uang Hilang Saat Stress Test

Saya membuat skrip sederhana untuk mensimulasikan trafik tinggi. Skenarionya:

  • Kirim 1.000 request Top-Up secara bersamaan (concurrent).
  • Nominal per request: Rp 1.000.
  • Ekspektasi Saldo Akhir: Rp 1.000.000.

Saya jalankan tesnya. Terminal berkedip cepat. Dan hasilnya seperti gambar diatas

Saldo yang masuk jauh di bawah Rp 1.000.000. Ada ratusan ribu rupiah yang "menguap" begitu saja.

Setelah debugging, saya sadar saya sedang berhadapan dengan musuh klasik dalam sistem keuangan: Race Condition.


Kenapa Bisa Hilang?

Dalam sistem konkurensi tinggi, fungsi Top-Up saya melakukan tiga langkah fatal tanpa pengaman:

  1. READ: Baca saldo saat ini (misal: 0).
  2. MODIFY: Tambah 1.000 di memori aplikasi (0 + 1.000 = 1.000).
  3. WRITE: Simpan saldo baru ke database (1.000).

Masalahnya, ketika 1.000 request masuk di milidetik yang sama, Request A dan Request B bisa jadi membaca saldo awal yang sama (Rp 0) sebelum salah satunya sempat menyimpan perubahan. Akibatnya, mereka saling menimpa (overwrite) data satu sama lain.

Saya sempat terpikir menggunakan sync.Mutex (Locking di level aplikasi Golang). Tapi saya sadar, solusi ini tidak scalable. Jika nanti service ini dijalankan di atas Kubernetes dengan banyak replika (Pod), Mutex di satu server tidak bisa menahan request di server lain.

Saya butuh solusi yang lebih fundamental. Solusi di level Database.


Database Pessimistic Locking

Setelah riset, saya menemukan konsep Pessimistic Locking. Intinya sederhana: "Kalau saya lagi baca data ini buat diedit, orang lain harus antre dulu." Di PostgreSQL, ini dilakukan dengan query SELECT ... FOR UPDATE. Di Golang (menggunakan GORM), implementasinya ternyata sangat elegan.

Saya memodifikasi layer Repository saya menjadi seperti ini:

code
// wallet-service/internal/repository/postgres_gorm.go

func (r *walletRepository) UpdateBalance(userID string, amount int64) error {
    return r.db.Transaction(func(tx *gorm.DB) error {
        var model WalletModel
        
        // 🔒 THE MAGIC LINE: Locking row database
        // Request lain dipaksa menunggu (queue) di sini sampai transaksi ini selesai.
        if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
            Where("user_id = ?", userID).
            First(&model).Error; err != nil {
            return err
        }

        // Aman untuk update saldo
        model.Balance += amount
        
        return tx.Save(&model).Error
    })
}

Baris tx.Clauses(clause.Locking{Strength: "UPDATE"}) adalah kuncinya. Ia memastikan operasi ini bersifat Serial, bukan Paralel, khusus untuk baris data user tersebut.


Hasil Akhir

Setelah menerapkan patch tersebut, saya menjalankan ulang Stress Test dengan parameter yang sama (1.000 concurrent requests).

Hasilnya?

  • Total Request: 1.000
  • Total Masuk: Rp 1.000.000
  • Selisih: Rp 0

Sistem sekarang berjalan 100% akurat. Latency pun tetap terjaga berkat konfigurasi Connection Pooling yang tepat di database.


Kesimpulan

Proyek ini mengajarkan saya bahwa coding backend itu bukan sekadar menghafal sintaks. Tantangan sesungguhnya ada pada pemahaman System Design, Concurrency Control, dan bagaimana menjaga Data Integrity di bawah tekanan trafik tinggi.

Bagi saya, melihat log terminal bersih dari error setelah memecahkan masalah Race Condition adalah kepuasan tersendiri yang sulit dijelaskan.

Jika teman-teman ingin melihat kode lengkapnya, termasuk implementasi Clean Architecture dan konfigurasi Docker-nya, silakan cek di GitHub saya.

🔗 Link Repo: Link Github

Happy Coding!

Tags

Engineering

Enjoyed this article?

Check out more writings

Back to Overview