Menyelaraskan cache dan session di tokokita: cache-aside Redis dengan invalidation lewat event, distributed session vs JWT yang stateless, memahami dan menerima eventual consistency, serta memvalidasi stok di saat checkout bukan hanya di UI

Setelah episode 14 menata sisi baca dengan read model, episode ini melengkapi sisi performa dan identitas: cache, session, dan konsistensi data terdistribusi. Kita akan memakai Redis secara penuh — bukan hanya untuk keranjang (episode 6), tetapi untuk katalog, rate limit, blacklist token, dan sinkronisasi antar instance.
Mengapa episode ini penting? Karena di microservices, data yang sama ada di banyak tempat: database, cache, read model, dan kantong user. Inilah ujian sesungguhnya: menerima bahwa tidak semua data bisa konsisten seketika, dan merancang di mana inkonsistensi boleh hidup serta di mana harus dimatikan.
Pola paling sederhana dan paling umum — baca cache, jika miss ambil dari source of truth lalu set cache:
export async function getProductCached(id: string) {
const key = `product:${id}`
const hit = await redis.get(key)
if (hit) return JSON.parse(hit)
const product = await db.query('select * from products where id = $1', [id])
await redis.set(key, JSON.stringify(product), { ex: 300 }) // TTL 5 menit
return product
}TTL adalah pengaman: paling buruk data basi 5 menit, dan key otomatis hilang tanpa cron. Ini adalah eventual consistency yang terbatas dan terkendali.
TTL saja tidak cukup untuk perubahan penting (harga turun, stok habis). Kita invalidasi cache via event — product-service publish product.updated, dan sebuah consumer menghapus key terkait:
consume('product-events', 'cache-invalidator-group', {
'product.updated': async (e) => {
await redis.del(`product:${e.data.id}`)
await redis.del('products:list') // list version pun ikut di-bust
},
})Perhatikan pilihan: cache-invalidate (hapus) lebih aman daripada cache-update (tulis cache dari event) karena menghindari race dengan writer DB. Consumer memakai consumer group berbeda agar tidak mengganggu projector read model.
Tip
Aturan praktis cache: read-through + TTL + invalidation via event. Tanpa TTL, satu bug lupa invalidasi membuat data basi permanen; tanpa invalidation, TTL panjang membuat harga lama terlihat lama. Keduanya wajib. Dan hati-hati cache stampede: saat key expire bersamaan dan 100 request miss bersamaan, sementara hanya 1 yang menulis cache (gunakan lock atau SETNX) di aplikasi dengan lalu lintas tinggi.
Kalau semua instance layanan memvalidasi JWT sendiri (cukup verifikasi tanda tangan — episode 4), tidak ada state di server untuk sesi. Keuntungan: scale-out horizontal tanpa shared session store. Kekurangan: revoke tidak instan (harus blacklist).
Jarang diungkit, tapi kombinasi emasnya:
blacklist:{jti} dengan TTL sampai expiry token.login → access (15m) + refresh (30d, httpOnly)
├─ tiap request: verifikasi access
├─ access exp → refresh → access baru + refresh baru (rotate; yang lama di-revoke)
├─ logout → blacklist access jti (TTL 15m) + revoke refresh
└─ reuse refresh lama setelah rotate → curigai pencurian → revoke semua sesi userDeteksi "rotated refresh dipakai lagi" adalah sinyal token curian — layak untuk mem-revoke seluruh sesi dan meminta login ulang (topik keamanan lanjutan episode 19).
Berapa parah keranjang boleh tidak sinkron dengan stok? Sangat longgar — keranjang hanyalah daftar keinginan. Yang penting: stok divalidasi saat checkout, bukan hanya di UI. UI boleh menampilkan stok basi 5 menit; yang tidak boleh basi adalah keputusan commit stok.
Validasi stok dua lapis:
POST /products/:id/reserve di product-service (langsung ke write path, bukan cache). Inilah yang mencegah oversell saat dua user checkout produk terakhir bersamaan.web-app (UI) → checkout → order-service
→ POST /products/:id/reserve (product-service, transaksi DB)
└─ validasi qty_available >= n — otoritatif, bukan dari cache
→ reserved → order CONFIRMED → paymentKesalahan klasik: menampilkan stok dari cache, lalu ... memutuskan stok dari cache yang sama saat checkout. Itu berarti data basi menjadi otoritas — malapetaka saat flash sale.
Episode 15 menyelaraskan cache, session, dan konsistensi:
jti adalah kunci revoke.Di episode 16 selanjutnya, kita pindah ke lapisan infrastruktur: container networking & Docker Compose untuk banyak service — bridge network dan DNS antar service, best practice healthcheck, named volumes, env via .env, dan pengamanan image container multi-stage non-root. Sampai jumpa di episode 16!