Saldo user itu bukan angka yang boleh lu update sembarangan. Kalau app lu punya wallet, credit, subscription balance, payout creator, atau point yang bisa ditukar value real, bug kecil di backend bisa berubah jadi uang nyasar, double charge, refund kacau, atau laporan finance yang nggak bisa dipercaya.

Artikel ini ngebahas kenapa backend developer perlu memperlakukan uang sebagai sistem, bukan sekadar kolom number di database. Jawaban singkatnya: uang butuh ledger, idempotency, audit trail, reconciliation, dan aturan reversal yang jelas. Bukan karena semua app harus jadi bank, tapi karena begitu produk lu menyentuh value finansial, cara CRUD biasa mulai berbahaya.

Gua ambil angle ini dari Fintech Engineering Handbook, diskusi Hacker News, dokumentasi Stripe soal idempotency dan webhook, plus konsep event sourcing dari Martin Fowler. Fokusnya engineering, bukan nasihat legal atau compliance. Kalau lu beneran bikin fintech, tetap butuh domain expert, legal, compliance, dan audit yang proper.

Masalah utamanya: uang bukan state biasa

Di banyak aplikasi, state bisa diedit langsung. Nama user salah? Update field. Status order berubah? Update status. Tapi uang beda. Kalau saldo user berubah dari 100 ribu ke 80 ribu, pertanyaan pentingnya bukan cuma “angka akhirnya berapa?”

Pertanyaan yang lebih penting:

  • Kenapa saldo berubah?
  • Event apa yang memicu perubahan itu?
  • Siapa atau sistem mana yang menjalankan perubahan?
  • Apakah perubahan ini bisa diulang tanpa double charge?
  • Kalau salah, reversal-nya seperti apa?
  • Apakah laporan internal cocok dengan payment provider?

Kalau jawaban lu cuma “ya tinggal update saldo”, sistem lu belum punya cerita yang cukup untuk dipercaya.

Definisi sederhana: apa itu ledger?

Ledger adalah catatan transaksi yang menjelaskan pergerakan value. Bukan cuma snapshot saldo terakhir. Dalam sistem yang lebih serius, saldo biasanya dihitung dari daftar entry: debit, credit, fee, refund, reversal, payout, adjustment, dan event lain yang relevan.

Analogi gampangnya: saldo rekening itu angka ringkasan, tapi mutasi rekening adalah cerita lengkapnya. Kalau ada masalah, orang nggak cukup lihat saldo akhir. Orang butuh lihat jalur perubahan yang bikin saldo itu sampai ke angka sekarang.

Di sinilah banyak sistem internal gagal. Mereka punya field balance, tapi nggak punya riwayat yang cukup. Begitu ada bug, finance nanya “kenapa angka ini berubah?”, developer cuma bisa buka log aplikasi yang mungkin sudah expired, terlalu noisy, atau nggak lengkap.

Jangan simpan uang sebagai float

Ini aturan klasik, tapi masih worth diulang: jangan simpan uang sebagai floating point. Floating point didesain untuk komputasi numerik yang punya toleransi error kecil, bukan untuk uang yang butuh presisi.

Praktik yang lebih aman biasanya:

  • Simpan nominal dalam unit terkecil, misalnya rupiah sebagai integer, atau sen kalau mata uangnya punya subunit.
  • Simpan currency secara eksplisit, misalnya IDR, USD, atau SGD.
  • Definisikan aturan rounding di satu tempat, bukan tersebar di frontend, backend, dan job worker.
  • Jangan campur nilai uang dengan diskon, pajak, fee, atau kurs tanpa model data yang jelas.

Masalah float kadang nggak kelihatan di demo. Tapi begitu transaksi banyak, rounding beda antar service, atau ada FX rate, selisih kecil bisa numpuk jadi masalah rekonsiliasi.

Idempotency: tombol bayar boleh diklik dua kali, charge jangan ikut dua kali

Idempotency berarti request yang sama bisa dikirim lebih dari sekali tanpa menghasilkan efek samping ganda. Dalam payment flow, ini penting banget karena retry itu normal. Network timeout, user double click, mobile connection putus, worker restart, webhook dikirim ulang — semua itu kejadian real.

Stripe punya dokumentasi khusus soal idempotent request karena problem ini fundamental. Kalau user klik bayar, lalu koneksi timeout, backend lu mungkin nggak tahu apakah charge berhasil atau gagal. Tanpa idempotency key, retry bisa bikin charge baru. Dengan idempotency key, sistem bisa mengenali bahwa request itu percobaan yang sama, bukan transaksi baru.

Checklist praktis buat flow uang:

  • Setiap operasi finansial penting punya unique operation ID atau idempotency key.
  • Retry job harus aman dijalankan ulang.
  • Webhook dari provider harus dianggap bisa datang lebih dari sekali.
  • State transition harus punya aturan jelas: pending, succeeded, failed, reversed, refunded.
  • Jangan cuma mengandalkan “kode ini biasanya jalan sekali”. Itu bukan desain sistem.

Webhook bukan sumber kebenaran yang selalu rapi

Banyak developer menganggap webhook payment provider itu seperti callback biasa: provider kirim event, backend update database, selesai. Realitanya lebih berantakan.

Webhook bisa datang terlambat, dikirim ulang, gagal diterima, atau datang dalam urutan yang bikin lu salah asumsi. Dokumentasi Stripe sendiri menekankan pentingnya membuat endpoint webhook yang bisa menerima event dan memprosesnya dengan aman. Artinya, webhook handler lu perlu idempotent, punya signature verification, dan punya cara memetakan event provider ke state internal.

Prinsip gua: webhook jangan diperlakukan sebagai “notif”. Perlakukan dia sebagai event eksternal yang perlu diverifikasi, dicatat, dan direkonsiliasi.

Reconciliation: sistem lu harus bisa dicocokkan dengan dunia luar

Reconciliation adalah proses mencocokkan catatan internal dengan sumber eksternal: payment provider, bank, invoice, payout report, atau laporan finance. Ini bagian yang sering dianggap boring, padahal justru penyelamat saat angka nggak cocok.

Kalau app lu punya wallet internal dan payment provider bilang ada 10.000 transaksi sukses hari ini, sistem lu harus bisa menjawab:

  • Apakah 10.000 transaksi itu tercatat di database internal?
  • Apakah nominalnya sama?
  • Apakah fee, tax, refund, dan dispute sudah masuk perhitungan?
  • Mana transaksi yang statusnya menggantung?
  • Apa tindakan aman untuk memperbaikinya?

Tanpa reconciliation, bug uang sering baru ketahuan dari komplain user atau laporan finance. Itu telat.

Audit trail: log aplikasi bukan pengganti catatan finansial

Log aplikasi berguna buat debugging, tapi jangan jadikan log sebagai satu-satunya audit trail. Log bisa berubah format, hilang karena retention, atau terlalu mentah untuk dipakai menjelaskan transaksi.

Audit trail yang baik biasanya punya struktur: siapa, kapan, operasi apa, nominal berapa, currency apa, reference ID apa, sebelum/sesudah status apa, dan alasan perubahan. Kalau ada manual adjustment, alasan dan aktornya harus jelas.

Konsep event sourcing dari Martin Fowler relevan di sini: alih-alih hanya menyimpan state akhir, sistem menyimpan rangkaian event yang membentuk state itu. Lu nggak harus selalu menerapkan event sourcing penuh. Tapi mindset-nya penting: untuk domain uang, perubahan state perlu bisa dilacak sebagai rangkaian kejadian, bukan sekadar overwrite angka terakhir.

Contoh bug yang kelihatan kecil tapi mahal

Bayangin lu bikin platform creator. User beli credit 100 ribu, lalu credit itu bisa dipakai buat akses konten premium. Payment provider sudah charge user, tapi webhook timeout. Worker retry. Karena nggak ada idempotency key yang benar, sistem mencatat pembelian dua kali. User dapat credit 200 ribu.

Di sisi lain, ada kasus kebalikannya: provider sudah sukses, tapi backend gagal update saldo. User sudah bayar, saldo nggak masuk. Kalau tim support nggak punya ledger dan reference ID yang jelas, investigasinya jadi tebak-tebakan.

Bug seperti ini jarang terlihat keren di demo, tapi sangat terasa di production. Dan begitu menyangkut uang, trust user turun lebih cepat daripada page load yang lambat.

Checklist sebelum app lu menyentuh wallet, credit, atau payout

  • Representasi uang: pakai integer/decimal yang tepat, bukan float.
  • Currency eksplisit: jangan anggap semua nominal otomatis IDR kalau roadmap bisa berubah.
  • Ledger: simpan riwayat pergerakan value, bukan cuma saldo akhir.
  • Idempotency: semua operasi penting aman terhadap retry dan double submit.
  • Webhook safety: verifikasi signature, dedupe event, dan handle out-of-order event.
  • Reconciliation: siapkan job atau report untuk mencocokkan provider vs internal.
  • Reversal/refund: jangan edit transaksi lama diam-diam; catat koreksi sebagai event baru.
  • Audit trail: setiap perubahan penting bisa dijelaskan ke support, finance, dan engineer lain.
  • Permission: manual adjustment harus dibatasi dan tercatat.
  • Testing: test retry, timeout, duplicate webhook, partial failure, dan race condition.

Takeaway buat backend developer

Kalau produk lu masih CRUD biasa, update field mungkin cukup. Tapi begitu produk lu mulai menyimpan value, pola pikirnya harus naik level. Uang bukan cuma angka. Uang adalah domain dengan invariant, histori, koreksi, dan konsekuensi nyata.

Menurut gua, ini pelajaran penting buat developer yang mau naik dari “bisa bikin fitur” ke “bisa bikin sistem yang dipercaya”. UI payment bisa dibuat cantik dalam sehari. Tapi ledger, idempotency, reconciliation, dan audit trail adalah bagian yang menentukan apakah sistem itu aman dipakai saat transaksi mulai ramai.

Kalau lu lagi bikin produk digital, marketplace kecil, SaaS credit, membership, atau payout creator, simpan artikel ini sebagai checklist awal. Bukan supaya lu over-engineer dari hari pertama, tapi supaya lu tahu bagian mana yang nggak boleh dianggap remeh.

FAQ singkat

Apakah semua app perlu ledger?

Nggak semua. Tapi kalau app lu menyimpan saldo, credit, payout, refund, atau value yang bisa diperdebatkan user, ledger mulai sangat masuk akal.

Apakah idempotency cuma buat payment?

Nggak. Idempotency berguna untuk banyak operasi yang bisa di-retry: kirim email, create order, generate invoice, payout, dan workflow otomatis. Tapi di payment, dampaknya lebih mahal kalau salah.

Apakah event sourcing wajib?

Nggak wajib. Tapi prinsip “catat perubahan sebagai event yang bisa diaudit” sangat berguna untuk domain uang. Lu bisa mulai dari ledger table dan audit trail yang rapi sebelum masuk event sourcing penuh.

Apakah artikel ini cukup untuk bikin fintech production?

Nggak. Ini checklist engineering awal. Fintech production tetap butuh compliance, security review, domain expert, legal, observability, incident response, dan proses audit yang serius.

Referensi / Bacaan Lanjut