Belajar Microservices - Cache, Session & Konsistensi Terdistribusi
Episode 15 of 28

Belajar Microservices - Cache, Session & Konsistensi Terdistribusi

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

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

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.

Caching Layer di Redis

Apa yang Dicache di tokokita

  • Product list / detail — katalog dibaca 20x lebih sering daripada ditulis.
  • User session / blacklist JWT — data berumur pendek.
  • Rate limit counters — counter per IP/user.

Cache-Aside: Get, Miss, Backfill

Pola paling sederhana dan paling umum — baca cache, jika miss ambil dari source of truth lalu set cache:

Cache-aside product detail
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.

Invalidation via Event

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:

Invalidasi cache via event
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.

Distributed Session vs JWT

JWT Stateless untuk Akses

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).

Refresh Token: Rotate + Blacklist di Redis

Jarang diungkit, tapi kombinasi emasnya:

  • Access token pendek (15 menit) — kerugian revoke dibatasi jendela kecil.
  • Refresh token diputar (rotate) setiap pemakaian — token yang dicuri hanya berlaku sekali.
  • Blacklist di Redis saat logout: blacklist:{jti} dengan TTL sampai expiry token.
Siklus hidup 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 user

Deteksi "rotated refresh dipakai lagi" adalah sinyal token curian — layak untuk mem-revoke seluruh sesi dan meminta login ulang (topik keamanan lanjutan episode 19).

Mengukur Inconsistensi: Kapan Boleh Loose

Eventual Consistency: Keranjang vs Stok

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.

Checkout Flow yang Benar

Validasi stok dua lapis:

  1. Di UI — untuk UX: item habis dimark dengan cepat (bisa dari cache/read model).
  2. Di backend — otoritatif: saat checkout, order-service memanggil POST /products/:id/reserve di product-service (langsung ke write path, bukan cache). Inilah yang mencegah oversell saat dua user checkout produk terakhir bersamaan.
Validasi stok lapis 2 (wajib)
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 → payment

Kesalahan klasik: menampilkan stok dari cache, lalu ... memutuskan stok dari cache yang sama saat checkout. Itu berarti data basi menjadi otoritas — malapetaka saat flash sale.

Penutup

Episode 15 menyelaraskan cache, session, dan konsistensi:

  • Redis dipakai penuh: katalog, blacklist token, rate limit, keranjang.
  • Cache-aside + TTL + invalidation via event — TTL sebagai pengaman maksimum, event untuk perubahan penting.
  • JWT stateless akses pendek + refresh rotate + blacklist Redis saat logout; token jti adalah kunci revoke.
  • Eventual consistency diterima untuk baca (keranjang, katalog) — tapi validasi stok checkout harus otoritatif di write path, bukan UI apalagi cache.

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!