Benchmark Payment Gateway Bagian 2 : Membangun Pembanding, dan Ketahuan Isinya Palsu

Di bagian pertama, kami sepakat di atas kertas untuk memilih arsitektur monolith relasional (PostgreSQL). Kendati demikian, kami tetap membangun versi pembanding berarsitektur event-sourced agar keputusan tersebut teruji dengan angka empiris. Repo pembanding ini diberi nama payment-gateway-evtsrc (open source, lisensi Apache 2.0). Implementasi awal beserta skenario pengujiannya sebagian besar dikerjakan oleh Gemini Flash melalui Antigravity CLI.

Sebelum menjadikan benchmark awal tersebut sebagai baseline yang valid, saya meminta Claude Code mengaudit repositori buatan Gemini Flash. Hasil audit mengungkap bahwa benchmark awal di repo tersebut ternyata belum mengukur proses transaksi secara nyata.

Arsitektur: Kafka Streams sebagai Single Writer

Sebelum mengulas hasil audit, mari pahami arsitekturnya. Sistem ini memanfaatkan Kafka dalam pola Event Sourcing + CQRS yang dirancang untuk domain transaksi keuangan:


flowchart TB
    subgraph Write["Write Path"]
        API["REST API
(pre-validasi lokal)"] --> Topic1["charge-events"] API --> Topic2["va-events"] API --> Topic3["payment-events"] end subgraph Streams["Kafka Streams Topology"] Topic1 --> ChargeStore[("charge-state-store
RocksDB")] Topic2 --> VaStore[("va-registry-store
RocksDB")] Topic3 --> Processor["PaymentEventProcessor
single writer per charge"] Processor --> ChargeStore Processor -->|"double settlement?"| Topic3 end subgraph Read["Read Path"] Topic1 --> Sink["Projection Sink"] Topic2 --> Sink Topic3 --> Sink Sink --> PG[("PostgreSQL
Read Model")] end

Elemen kunci dari arsitektur ini meliputi:

  • RocksDB sebagai state store lokal. Kafka Streams menyimpan salinan state (status tagihan, pendaftaran nomor VA, serta data idempotency) pada embedded RocksDB di tiap instance. Data lokal inilah yang dibaca langsung oleh REST API saat validasi, tanpa perlu melakukan roundtrip ke database utama.
  • Satu writer per partition key. Seluruh event untuk satu chargeId selalu dialokasikan ke partition yang sama dan diproses secara berurutan oleh satu thread. Mekanisme antrean sekuensial ini menjamin hanya ada satu penentu status untuk tagihan tertentu.
  • PostgreSQL sebagai read model. Konsumen Kafka terpisah (PostgresProjectionSink) bertugas membaca event-event tersebut lalu memproyeksikannya ke tabel PostgreSQL untuk kebutuhan query dan pelaporan. Inilah pemisahan total antara jalur perintah (command path) dan jalur query (query path).

Protokol yang diuji dibuat identik dengan versi produksi Opsi A: menggunakan adapter bank BSI, enkripsi checksum SHA-1, serta format request/response yang sama persis. Enam kombinasi VA dan nominal yang sama diterapkan pada kedua sisi agar perbandingannya berjalan setara.

Temuan Audit Claude Code

Audit tersebut mengungkap tujuh isu krusial (diberi kode F1 hingga F7 pada dokumen internal kami), dengan tiga temuan paling fatal:

F1 — Validasi absen pada jalur utama (hot path). Endpoint yang diuji ternyata mengembalikan respon sukses tanpa memeriksa keabsahan nomor VA, status pelunasan, maupun potensi transaksi duplikat. Angka p99 yang terukur sebatas mencerminkan latensi respon HTTP kosong saat memproses payload.

F2 — Topologi Streams tidak menjamin invariant apa pun. Kafka Streams memang berjalan, tetapi tidak mengambil keputusan apa pun terkait status transaksi. Jaminan single writer per partition yang digadang-gadang sebagai keunggulan Opsi B pada Bagian 1 tidak dimanfaatkan di level kode.

F5 — Skrip audit koreksi bersifat tautologis. Skrip Python yang diklaim memverifikasi ketiadaan pembayaran ganda ternyata hanya menghitung ulang data dari jalur penulisan yang sedang diuji. Secara matematis skrip tersebut tak akan pernah gagal — mirip seperti memverifikasi kalkulator dengan menanyakan jawabannya ke kalkulator itu sendiri.

Selain itu, skrip k6 yang digunakan untuk menguji kedua arsitektur (RDBMS dan event-sourced) rupanya memiliki perbedaan struktur payload dan skema otentikasi. Akibatnya, angka latency p99 keduanya tidak bisa dibandingkan secara apple-to-apple sejak awal.

Evaluasi Kelolosan Benchmark

Semua unit tes berstatus hijau, pengujian selesai tanpa kendala, dan angkanya pun sekilas tampak meyakinkan. Namun, status tes yang hijau perlu dipahami secara kritis: uji coba yang dirancang tanpa kemungkinan gagal hanya menandakan adanya masalah pada metode pengujian.

Pelajaran ini berulang sepanjang proyek: tampilan hasil yang rapi memerlukan verifikasi mendalam. Pola kekeliruan serupa muncul di kemudian hari — termasuk pada Bagian 3 dan 4, ketika skenario pengujian yang dikira sudah bersih kembali terkecoh oleh variabel lain.

Remediasi: Membangun Ulang Sistem yang Valid

Rangkaian temuan tersebut dirangkum ke dalam panduan perbaikan (benchmark-remediation-guideline.md, mencakup poin G1 hingga G8). Langkah pembenahan kemudian dieksekusi secara sistematis bersama Claude Code:

  • Pre-validasi di request thread: memeriksa idempotency, melacak registrasi nomor VA, serta mengonfirmasi status tagihan sebelum event dimasukkan ke Kafka.
  • Topologi yang menjamin aturan domain dipatuhi secara konsisten: PaymentEventProcessor dijadikan satu-satunya penentu status akhir per tagihan — kelas ini kemudian dihapus total saat rearsitektur di Bagian 6, tautan di atas menunjuk ke commit terakhir sebelum dihapus. Jika terjadi keterlambatan pembayaran, sistem menerbitkan DoubleSettlementDetectedEvent untuk mencatat anomali transaksi tersebut.
  • Adapter BSI yang presisi meniru behavior di production, lengkap dengan kalkulasi checksum SHA-1.
  • Skrip audit independen: membandingkan log eksekusi k6 (ground truth dari luar sistem) dengan data yang tersimpan di database.
  • Penyelarasan skrip k6 untuk kedua arsitektur: menyamakan struktur payload, mekanisme checksum, hingga profil beban (ramp-up TPS).

Setelah perbaikan selesai, saya melakukan pengujian ulang terhadap klaim “21 dari 21 tes lulus”. Masih terdapat tiga kasus uji yang gagal saat dieksekusi secara terisolasi (terkait race condition saat pemulihan state, sisa data direktori RocksDB antar-pengujian, serta race condition pada idempotency). Temuan ini mempertegas bahwa laporan “semua lulus” dari pihak mana pun wajib diverifikasi ulang secara mandiri.

Pembenahan menyeluruh ini menghasilkan landasan komparasi yang valid. Bagian selanjutnya akan membedah angka benchmark valid yang pertama — sekaligus mengulas satu bug domain krusial yang terungkap saat kedua implementasi diuji secara berdampingan.

Stay tuned …