Payment Gateway Virtual Account, Episode 2 dari 5

PostgreSQL atau Kafka?
Membandingkan Dulu Sebelum Membangun

Endy Muhardin, ArtiVisi Intermedia

"Mengapa tidak event sourcing saja?"

Setiap kali ArtiVisi mengembangkan sistem yang mengelola uang, pertanyaan ini selalu muncul dengan alasan yang sama:



Di seri ini, monolit PostgreSQL dan event sourcing dengan Kafka sama-sama dibuat lalu diuji berdampingan, sehingga keputusan arsitektur diambil berdasarkan data hasil pengukuran.

Produk: Gateway Virtual Account Multi-Bank

Untuk siapa

  • Institusi yang terhubung langsung ke beberapa bank
  • SPP kampus, tagihan rumah sakit, yayasan
  • Pembayaran masuk langsung ke rekening penampungan milik institusi, tanpa rekening perantara

Mengapa bukan agregator

  • Biaya per transaksi yang nominalnya besar untuk tagihan sebesar SPP
  • Tanpa agregator, institusi berintegrasi sendiri dengan tiap bank, masing-masing dengan protokol dan format data yang berbeda, lalu merekonsiliasi transaksi tiap bank secara terpisah
  • Gateway self-hosted ini menyediakan satu API untuk banyak bank, tanpa biaya selain biaya bank

Aturan Bisnis yang Menentukan Arsitektur


Poin terakhir menjadi inti drama di episode 4. Simpan dulu.

Masalahnya: Satu Tagihan, Dua Pembayaran

dua-duanya membaca sebelum ada yang menulisRequest 1pembayaran lewat Bank ARequest 2pembayaran lewat Bank BTagihan #123CLOSED · Rp 2.000.000AKTIF1. baca statusAKTIF2. baca statusAKTIF3. catat pembayaran, status jadi LUNASLUNAS4. catat pembayaran, status jadi LUNASLUNAS, dua kaliRp 4.000.000 masuk untuk tagihan Rp 2.000.000dua pembayaran tercatat sebagai pelunasan, dan tidak ada yang menandainya

Tanpa pengaman, kedua request membaca status AKTIF sebelum salah satunya sempat menulis LUNAS. Keduanya lalu mencatat pelunasan untuk tagihan yang sama. Opsi A dan Opsi B menjawab masalah ini dengan cara yang berbeda.

Opsi A: Monolit Relasional

Request 1pembayaran lewat Bank ARequest 2pembayaran lewat Bank BTagihan #123CLOSED · Rp 2.000.000AKTIF1. kunci baris tagihankunci didapat2. kunci baris tagihanmenunggubaca status: AKTIFcatat pembayaran, status jadi LUNAS3. commit, kunci dilepasLUNASjawab bank: diterimakunci didapatbaca status: LUNASjawab bank: ditolakpembayaran ganda dicegah

Dua request mengakses baris tagihan yang sama. Row lock membuat request kedua menunggu, lalu membaca status yang sudah diperbarui.

Mekanisme: validasi dan pencatatan dalam satu transaksi database, dengan SELECT FOR UPDATE.

Opsi B: Event-Sourced, CQRS (Command Query Responsibility Segregation)

Request 1pembayaran lewat Bank ARequest 2pembayaran lewat Bank BPemroses tunggalmemproses event tagihan #123 satu per satuAKTIF1. kirim event bayar #1menunggu keputusan2. kirim event bayar #2menunggu keputusanproses event #1:AKTIF menjadi LUNASkeputusan #1: terimaLUNASjawab bank: diterimaproses event #2: sudah LUNASkeputusan: tolakkeputusan #2: tolakjawab bank: ditolakpembayaran ganda dicegah

Kedua request mengirim event, lalu menunggu keputusan pemroses tunggal sebelum menjawab bank. Pemroses tunggal menangani event untuk tagihan yang sama satu per satu. Saat event #2 diproses, status tagihan sudah LUNAS, sehingga pembayaran kedua ditolak.

Mekanisme: event dikirim ke Kafka dengan key ID tagihan, diproses Kafka Streams sebagai single writer per tagihan dengan status di RocksDB, lalu keputusannya dikembalikan ke request. PostgreSQL hanya menjadi read model.

Trade-off di Atas Kertas, Sebelum Ada Kode

DimensiOpsi AOpsi B
Ingress protokolSynchronous request-response langsungSynchronous, validasi lokal dulu, lalu append async ke Kafka
Source of truthRelational databaseEvent log + RocksDB state store lokal
Concurrency controlRow lock per transaksiSingle writer per partition key, tanpa lock
Jaminan satu tagihan hanya lunas sekali (invariant)Atomic multi-row transaction, bawaanBisa atomic, butuh desain yang tepat
Overhead operasional1 app + 1 databaseTambah 1 Kafka broker + komponen streaming

docs/architecture-comparison.md di repo payment-gateway, ditulis sebelum baris kode pertama.

Menjamin Satu Tagihan Hanya Lunas Sekali

Opsi A

  • PostgreSQL sudah menyediakan antrean lock, sehingga pembayaran kedua untuk tagihan yang sama menunggu hingga transaksi pembayaran pertama selesai
  • Tanpa desain ekstra

Opsi B

  • Request harus menunggu keputusan pemroses tunggal, atau validasi dan pencatatan disatukan dalam satu transaksi RocksDB
  • Butuh desain ekstra
  • Pola yang umum dipakai, validasi di state lokal lalu append event, tidak menunggu keputusan itu, sehingga Bank B telanjur dijawab diterima sebelum pembayaran ganda terdeteksi
  • Jeda ini akibat pola implementasi, bukan keterbatasan RocksDB atau Kafka Streams

Di atas kertas, Opsi A yang dipilih. Beban puncak institusi berkisar puluhan hingga ratusan TPS (transaksi per detik).

Mengapa Opsi A dan Opsi B Tetap Dibuat

Syarat Agar Perbandingannya Setara

Sebelum melihat angka apa pun, perbandingan kedua opsi harus memenuhi enam syarat ini.

Siapa Mengerjakan Apa

BagianDikerjakan oleh
Opsi A, versi productionClaude Code
Opsi B, versi awal beserta skenario benchmarkGemini Flash lewat Antigravity CLI
Audit, perbaikan, eksekusi benchmarkClaude Code

Seri ini turut merekam bagaimana satu AI agent menguji dan membenahi hasil kerja agent lain.

Laporan Perbandingan Pertama

Laporan ini ditulis di repo Opsi B, sebelum ada audit. Isinya membandingkan kedua opsi dan menyatakan keduanya diuji dalam kondisi identik.

MetrikOpsi AOpsi B
Median latency0,57 ms0,68 ms
p9916,26 ms18,45 ms
Request sukses100%100%
Pembayaran ganda00
Audit keuangan100% PASS100% PASS

Selisihnya tipis dan wajar. Apakah angka ini bisa dipercaya?

scenarios/perf_benchmark_report.md §8.1 di repo payment-gateway-evtsrc, versi sebelum audit (commit b0cad3d).

Episode berikutnya


software.endy.muhardin.com/programming/kafka-vs-postgresql-01-arsitektur-payment-gateway/

github.com/artivisi/payment-gateway · github.com/artivisi/payment-gateway-evtsrc