Kenapa stok Storio tidak memakai event sourcing
Kenapa Storio tidak memakai event sourcing di jalur stok terpanas — trade-off audit sempurna vs latensi ritel.
Pertanyaan yang cukup sering datang dari tim teknis calon klien: "Stoknya event-sourced, kan?" Nadanya biasanya berharap jawabannya ya, karena event sourcing terdengar seperti jawaban yang benar untuk sistem yang butuh jejak audit.
Jawaban kami tidak. Dan penolakan itu keputusan sadar yang kami tulis sebagai keputusan arsitektur, bukan hasil kurang waktu.
Apa yang dijanjikan event sourcing
Dalam sistem event-sourced, angka stok tidak disimpan. Yang disimpan adalah peristiwa:
BarangDiterima { sku: TSH-BLK-M, qty: +50, po: PO-2291 }
TerjualDiPOS { sku: TSH-BLK-M, qty: -1, outlet: BDG-01 }
TransferKeluar { sku: TSH-BLK-M, qty: -8, tujuan: JKT-03 }
Stok saat ini adalah hasil melipat seluruh peristiwa dari awal waktu. Tidak ada kolom yang pernah ditimpa, jadi tidak ada sejarah yang bisa hilang. Untuk sistem yang pertanyaan terpentingnya "kenapa angkanya jadi segini", ini terdengar seperti pasangan yang sempurna.
Kenapa kami tetap menolaknya
Karena ritel punya satu jalur yang tidak boleh lambat, dan event sourcing membebani persis jalur itu.
Setiap penjualan di kasir perlu tahu stok tersedia sekarang juga. Kalau angka itu harus dihitung dengan melipat ribuan peristiwa, Anda butuh lapisan-lapisan tambahan agar tetap cepat: snapshot berkala, proyeksi yang dipelihara terpisah, dan compaction untuk peristiwa lama. Masing-masing punya kemungkinan gagal sendiri, dan ketika proyeksi melenceng dari peristiwanya, Anda tidak punya satu angka yang bisa dipercaya — Anda punya dua.
Kerumitan itu sepadan bila kebutuhan audit Anda tidak bisa dipenuhi cara lain. Ternyata bisa.
Yang kami pakai: kolom yang di-update, plus log yang tidak pernah disentuh
Stok disimpan sebagai angka yang memang di-update. Di sebelahnya, setiap mutasi menulis satu baris ke log append-only — tanpa update, tanpa delete, selamanya.
Tiap perubahan stok karena itu menghasilkan dua tulisan dalam satu transaksi yang sama: angka berjalannya bergerak, dan catatan permanen tentang pergerakan itu lahir. Kalau salah satunya gagal, keduanya batal.
| Angka stok | Log mutasi | |
|---|---|---|
| Sifat | Di-update di tempat | Append-only, tak pernah diubah |
| Dipakai untuk | Keputusan real-time di kasir | Audit, kartu stok, penelusuran |
| Biaya baca | Satu baris | Rentang baris, tapi jarang di jalur panas |
Hasilnya: kasir membaca satu angka dan langsung jalan, sementara pertanyaan "kenapa angkanya jadi segini" tetap terjawab lengkap dari log. Kita mendapat manfaat utama event sourcing tanpa memindahkan biayanya ke jalur checkout.
Di atas log mentah itu ada kartu stok dengan saldo berjalan yang sudah siap dibaca manusia — bentuk yang dikenal siapa pun yang pernah memegang buku persediaan, bukan aliran peristiwa yang perlu diterjemahkan lebih dulu.
Bagian yang paling sering diremehkan: dua kasir, satu barang terakhir
Begitu stok berupa angka yang di-update, muncul pertanyaan klasik: apa yang terjadi kalau dua transaksi mengurangi baris yang sama pada saat bersamaan?
Jawaban yang salah adalah membiarkannya. Hasilnya oversell — dua pelanggan membeli barang yang cuma ada satu, dan masalahnya baru ketahuan di gudang.
Kami menguncinya per pasangan (gudang, SKU). Dua kasir yang menjual artikel berbeda tidak saling menunggu sama sekali; yang menjual artikel sama akan diurutkan, satu berhasil dan satu lagi mendapat penolakan stok yang jelas — bukan hasil yang tidak terdefinisi.
Kuncinya sengaja tidak mengunci baris datanya, sehingga dasbor dan kueri kartu stok tetap bisa membaca tanpa ikut tertahan. Laporan tidak pernah memperlambat kasir, dan kasir tidak pernah memblokir laporan.
Ada satu lapis pengaman lagi: pengurangan stok tetap menyertakan syarat bahwa jumlah tersedia harus mencukupi. Kalau suatu hari lapisan kunci terlewati karena bug, basis data tetap menolak angkanya jatuh di bawah nol.
Stok tersedia bukan stok fisik
Satu pembedaan yang menyelamatkan banyak perdebatan operasional: kami menyimpan jumlah fisik dan jumlah yang dipesan secara terpisah, lalu yang dipakai untuk memutuskan boleh-tidaknya menjual adalah selisih keduanya.
Barang yang sudah dialokasikan untuk pesanan online masih berdiri di rak — ia nyata secara fisik, tapi tidak boleh dijual ulang di kasir. Tanpa pemisahan ini, setiap sistem cepat atau lambat menjual barang yang sebenarnya sudah menjadi milik orang lain.
Yang tetap kami curigai
Pendekatan mana pun punya titik lemah, dan titik lemah pendekatan kami jelas: angka berjalan bisa menyimpang dari log, entah karena bug, gangguan, atau migrasi yang tidak mulus.
Karena itu ada pekerjaan rekonsiliasi yang berjalan berkala di jam sepi, membandingkan angka berjalan dengan yang seharusnya, dan melaporkan selisihnya sebagai peringatan. Bukan karena kami menduga ada yang salah, melainkan karena sistem yang tidak pernah memeriksa dirinya sendiri hanya menunda penemuan masalah sampai pelanggan yang menemukannya.
Pelajarannya
Arsitektur yang lebih canggih bukan otomatis arsitektur yang lebih benar. Event sourcing menyelesaikan masalah audit dengan sangat baik — tapi kami punya masalah audit yang lebih sederhana daripada yang ia rancang untuk selesaikan, dan punya batasan latensi yang lebih ketat daripada yang ia nyaman tanggung.
Log append-only memberi kami jejak yang utuh. Kunci per (gudang, SKU) memberi kami kebenaran di bawah beban. Rekonsiliasi berkala memberi kami cara mengetahui saat keduanya tidak lagi sepakat.
Di ritel, kepercayaan pada angka memang segalanya. Tapi kepercayaan itu dibangun dari pemeriksaan yang jujur, bukan dari memilih pola arsitektur yang paling terdengar meyakinkan.