Studi Kasus Golang: Bagaimana Saya Menyelamatkan Saldo User dari Race Condition (1.000 Transaksi/Detik)
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:
- READ: Baca saldo saat ini (misal: 0).
- MODIFY: Tambah 1.000 di memori aplikasi (0 + 1.000 = 1.000).
- 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:
// 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
Enjoyed this article?