Benchmark Payment Gateway Bagian 7 : Status per VA, Bukan Cuma per Tagihan
Bagian 6 ditutup dengan kalimat “kali ini betulan selesai — sampai draf berikutnya ditinjau ulang lagi.” Ternyata ungkapan itu bukan sekadar basa-basi. Saat merapikan poin-poin bernomor di Bagian 4 — yang menjelaskan cara kerja transaksi atomic pada RocksDB — muncul satu pertanyaan yang rupanya membongkar celah nyata di dalam kode, bukan sekadar ketidaktepatan pilihan kata.
Pertanyaan yang Muncul Saat Merapikan Daftar
Daftar poin di Bagian 4 menjelaskan tiga langkah dalam satu transaksi RocksDB: mencari chargeId dari nomor VA, mengambil seluruh VA alternatif (sibling VA) untuk chargeId tersebut, lalu menandai semuanya lunas. Ketika kalimat ini dirapikan agar lebih nyaman dibaca, timbul pertanyaan mendasar yang mestinya sudah disadari sejak awal: mengapa semua VA harus ditandai lunas? Padahal, yang benar-benar menerima dana hanyalah satu VA di bank yang digunakan untuk membayar. VA alternatif di bank lain sama sekali tidak menerima pembayaran.
Jawabannya langsung mengarah pada repositori pembanding, payment-gateway (versi RDBMS). Di sana, method PaymentApplicationService.settleAndCancelSiblings() menjalankan alur berikut: VA yang menerima dana ditandai PAID, sedangkan setiap VA alternatif yang masih ACTIVE ditandai CANCELLED — bukan PAID. Ada dua alasan utama di balik pendekatan ini:
- Faktual: Hanya ada satu VA yang benar-benar menerima transaksi dana. Menandai VA alternatif sebagai “lunas” adalah kekeliruan fatal secara pembukuan.
- Operasional: Nomor VA dirancang untuk dapat digunakan kembali (sebagaimana tercantum dalam
CLAUDE.md: “VA numbers are reusable”). StatusPAIDdanCANCELLEDbukanlah dua hal yang bisa disamakan; meski keduanya menandakan bahwa “generasi VA ini sudah tidak aktif,” hanya statusCANCELLEDyang benar secara faktual untuk VA yang tidak pernah menerima dana.
Ternyata Bukan Cuma Soal Kalimat
Saat memeriksa kode ChargeSettlementStore untuk memastikan kebenaran implementasinya, ternyata masalahnya tidak sekadar kekeliruan narasi di blog — kodenya sendiri memang belum pernah membedakan status untuk tiap-tiap VA (tautan menunjuk pada commit terakhir sebelum diperbaiki). Data yang tersimpan baru sebatas status di level tagihan (ACTIVE/PAID/CANCELLED), bukan status spesifik masing-masing VA. Lebih parahnya lagi, InquiryApplicationService ternyata sama sekali tidak mengecek status VA — proses inquiry pada VA milik tagihan yang sudah lunas tetap saja mengembalikan respon SUCCESS beserta nominal tagihannya. Ini jelas menyalahi aturan domain pada CLAUDE.md: “a cancelled VA’s next inquiry returns NOT_FOUND.”
Perbaikan yang Dilakukan
Solusinya adalah menambahkan penanda status untuk tiap VA (ACTIVE/PAID/CANCELLED) beserta indeks baru (va_by_charge:). Dengan indeks ini, transaksi pelunasan tagihan dapat menelusuri seluruh VA alternatif tanpa perlu memindai (scan) keseluruhan store. Begitu pembayaran melunasi tagihan (baik jenis CLOSED maupun INSTALLMENT yang sudah mencapai total nominal), transaksi atomic secara bersamaan menandai VA pembayar sebagai PAID dan mengubah status seluruh VA alternatif yang masih ACTIVE menjadi CANCELLED — persis seperti perilaku settleAndCancelSiblings. InquiryApplicationService kini turut memeriksa status spesifik VA beserta status tagihannya, sehingga inquiry lanjutan terhadap VA yang sudah lunas maupun VA alternatif yang dibatalkan akan secara konsisten mengembalikan respon NOT_FOUND.
Implementasinya dapat dilihat langsung pada ChargeSettlementStore.settleSiblingVas() dan InquiryApplicationService.inquireAccount().
Regresi yang Langsung Tertangkap Test Suite
Perubahan format nilai yang tersimpan pada kunci VA — dari string chargeId sederhana menjadi objek JSON yang memuat status — sempat terlewat di satu bagian: method applyPayment() memiliki alur pembacaan VA tersendiri yang masih menggunakan format lama. Begitu pengujian dijalankan, seluruh test suite yang tadinya lolos (hijau) mendadak gagal (merah) — bukan cuma test yang baru ditambahkan. Ini menjadi pengingat berharga: test suite yang komprehensif bukan sebatas formalitas atau kelengkapan checklist, melainkan jaring pengaman efektif yang mampu menangkap regresi dalam hitungan detik sebelum sempat masuk ke commit, apalagi lingkungan production.
Verifikasi Lapangan
Guna memastikan perbaikan ini, dibuat sebuah test integrasi baru. Skenarionya: membuat tagihan CLOSED yang memiliki dua pilihan VA di bank berbeda, melunasinya melalui salah satu VA, lalu memastikan kedua VA tersebut — baik yang digunakan untuk membayar maupun VA alternatifnya — sama-sama mengembalikan respon HTTP 404 saat di-inquiry kembali. Pembuktian ini tak hanya mengandalkan unit test, melainkan juga dikonfirmasi secara langsung pada instance aplikasi yang sedang berjalan:
$ curl -X POST http://localhost:8080/api/v1/inquiry \
-d '{"bankCode":"BSI","vaNumber":"99012026001"}'
{"status":"INVALID_VA","message":"...no longer active (status PAID)"}
HTTP 404
Pengujian Ulang Benchmark — dan Jebakan Kontaminasi yang Berulang
Penambahan langkah baru (pemindaian prefix, operasi getForUpdate, serta serialisasi JSON untuk setiap VA alternatif) ke dalam transaksi atomic yang diakses langsung oleh request thread bank menuntut adanya pengukuran beban kerja yang nyata. Kita tidak boleh sekadar berasumsi “dampaknya pasti kecil karena hanya ada satu VA alternatif dalam skenario benchmark ini.” Oleh karena itu, pengujian benchmark wajib dijalankan kembali.
Sayangnya, percobaan pertama pada run kedua sempat terkontaminasi oleh eksekusi test suite Maven dari proyek lain yang berjalan bersamaan di mesin yang sama — mengulang persis kendala yang pernah terjadi pada Bagian 3. Percobaan kedua pun mengalami kontaminasi serupa akibat adanya sesi kerja aktif lain di komputer lokal, ditambah kendala tak terduga: container Kafka yang digunakan untuk pengujian mendadak berhenti di tengah jeda pengujian tanpa ada peringatan. Kedua hasil tersebut akhirnya dibuang, lingkungan pengujian di-reset total, dan benchmark diulang kembali dalam kondisi yang benar-benar steril:
| Metrik | Run 1 | Run 2 | Bagian 6 (pembanding) |
|---|---|---|---|
| p99 latency | 9,19 ms | 8,65 ms | 9,96 ms / 9,85 ms |
| VU puncak (dari 100 dialokasikan) | 46 | 31 | 32 / 33 |
| Audit akurasi | LULUS, 0 ketidaksesuaian | LULUS, 0 ketidaksesuaian | LULUS, 0 ketidaksesuaian |
Hasilnya menunjukkan tidak ada penambahan latency atau beban yang signifikan. Hal ini wajar karena skenario benchmark saat ini baru memuat satu VA alternatif per tagihan. Namun sebagai catatan objektif: dampak performa ketika membatalkan banyak VA alternatif secara bersamaan dalam satu penyelesaian tagihan memang belum terukur dalam benchmark ini. Meski diperkirakan tetap efisien untuk jumlah bank yang umum (2–3 bank), hal tersebut masih berupa hipotesis yang belum dibuktikan dengan data empiris.
Pembenahan Kode dan Dokumentasi
Proses menelisik satu celah sering kali mengungkap hal-hal lain di sekitarnya yang perlu dirapikan. Dua penyesuaian berikut turut dikerjakan:
- Pembersihan Kode Mati (Dead Code): Class
InitialStateSeeder— berbeda dariPostgresInitialStateSeederyang masih aktif digunakan — hanya bertugas menerbitkan event Kafka yang tidak lagi dikonsumsi oleh komponen manapun dalamChargeSettlementStoresejak rearsitektur di Bagian 6. Kode ini resmi dihapus karena tidak lagi memiliki fungsi operasional. - Pembaruan README yang Usang: Enam diagram dan beberapa bagian dokumen masih mengacu pada arsitektur lama — seperti klaim penggunaan tiga state store RocksDB oleh Kafka Streams, klaim latency “<1ms” yang bertolak belakang dengan data empiris benchmark, hingga skenario failover otomatis yang tidak lagi berlaku. Dokumentasi tersebut ditulis ulang secara menyeluruh, termasuk memberikan catatan terbuka bahwa arsitektur saat ini belum mendukung high-availability multi-instance pada jalur penulisan data.
Penutup yang (Mungkin) Belum Benar-Benar Berakhir
Ini adalah kali kedua secara berturut-turut di mana proses meninjau draf tulisan justru mengungkap celah teknis yang nyata di dalam kode, bukan sekadar masalah tata bahasa. Temuan di Bagian 6 muncul saat merevisi Bagian 5, sementara perbaikan di bagian ini lahir dari penelusuran ulang poin-poin di Bagian 4. Ini tentu bukan kebetulan, bukan pula pertanda bahwa seri ini akan berputar tanpa ujung.
Fenomena ini adalah buah dari komitmen untuk menjadikan proses “menjelaskan arsitektur kepada pembaca” sebagai metode evaluasi teknis yang sungguhan, bukan sebatas dokumentasi formalitas belakangan. Ketika kita berani menelaah alur berpikir secara jujur dan transparan, tak jarang kita justru menemukan hal-hal penting yang sebelumnya luput dari pengamatan.
Seluruh kode program, skenario test, hingga laporan benchmark lengkap dapat diakses secara terbuka pada repositori payment-gateway-evtsrc dengan lisensi Apache 2.0. Terima kasih banyak telah menyimak seri tulisan ini hingga akhir!