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.
Saya membuat skrip sederhana untuk mensimulasikan trafik tinggi. Skenarionya:
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.
Dalam sistem konkurensi tinggi, fungsi Top-Up saya melakukan tiga langkah fatal tanpa pengaman:
Enjoyed this article?
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.
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.
Setelah menerapkan patch tersebut, saya menjalankan ulang Stress Test dengan parameter yang sama (1.000 concurrent requests).
Hasilnya?
Sistem sekarang berjalan 100% akurat. Latency pun tetap terjaga berkat konfigurasi Connection Pooling yang tepat di database.
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!