Membangun cart-service berbasis Redis: keranjang per session guest dan per user, TTL 7 hari, snapshot harga saat add-to-cart, serta handoff keranjang ke order-service saat checkout tanpa menyentuh stok

Setelah auth-service (episode 4) dan product-service (episode 5), kini kita membangun cart-service — keranjang belanja tokokita. Layanan ini mengajarkan pelajaran penting tentang data yang berumur pendek, sangat sering dibaca/ditulis, dan bisa dimiliki oleh siapa pun — termasuk pengunjung yang belum login.
Mengapa cart-service menarik? Karena di sinilah kita pertama kali serius bergantung pada Redis sebagai primary store, bukan sekadar cache. Keranjang adalah data transien tempat menyimpan state tidak terstruktur; memasukkannya ke PostgreSQL (seperti di monolith) sering kali overkill. Kita akan melihat kapan sebuah layanan lebih baik dibangun di atas penyimpanan cepat daripada database relasional.
cart_token) atau per user (login, pakai user_id dari token).TypeScript + Bun + Elysia
Redis (primary store keranjang, cepat + TTL)
PostgreSQL (opsional: audit event cart untuk analitik/list)Dua identitas pemilik keranjang:
cart_token) yang dibuat pertama kali user membuka situs. Tidak butuh login untuk mulai belanja.user_id. Saat login, isi guest cart di-merge ke user cart agar barang yang ditambahkan sebelum login tidak hilang.guest: users:guest:cart:{cart_token}
user: users:{user_id}:cartItem keranjang disimpan sebagai Redis Hash (per item, qty), dengan key keranjang + TTL 7 hari (untuk guest):
import { createClient } from 'redis'
const redis = createClient({ url: process.env.REDIS_URL })
export async function addItem(cartKey: string, productId: string, qty: number) {
const pipe = redis.multi()
pipe.hIncrBy(cartKey, productId, qty)
pipe.expire(cartKey, 60 * 60 * 24 * 7) // TTL 7 hari
await pipe.exec()
}
export async function getCart(cartKey: string) {
return redis.hGetAll(cartKey)
}
export async function removeItem(cartKey: string, productId: string) {
await redis.hDel(cartKey, productId)
}TTL 7 hari untuk guest cart adalah keputusan produk, bukan teknis: setelah 7 hari tanpa aktivitas, anggap keranjang dibuang. Bunuh TTL otomatis membersihkan key yang tidak terpakai tanpa cron job — salah satu kekuatan Redis.
GET /cart → daftar item + subtotal
POST /cart/items → tambah item { productId, qty }
PATCH /cart/items/:id → ubah qty
DELETE /cart/items/:id → hapus item
DELETE /cart → kosongkan keranjangSemua endpoint menerima identitas lewat header khusus (x-cart-token untuk guest atau Authorization untuk user). Contoh add-to-cart:
export const addItemHandler = async ({ body, headers, set }: Context) => {
const cartKey = resolveCartKey(headers) // guest token atau user_id
const product = await productsApi.getProduct(body.productId) // via gateway/product-service
if (!product) {
set.status = 404
return { error: 'product not found' }
}
// snapshot harga + data produk agar cart mandiri (tidak query live terus)
await cartRepo.upsertItem(cartKey, product, body.qty)
set.status = 200
return await cartRepo.getCart(cartKey)
}Perhatikan: kita menyimpan snapshot harga saat add-to-cart, bukan query harga live setiap kali menampilkan keranjang. Alasan:
Snapshot harga disimpan sebagai field price di dalam item:
{
productId, name, price (snapshot), imageKey, qty
}Cart-service tidak menyentuh stok produk — sesuai prinsip episode 5. Menambah item ke keranjang tidak memvalidasi stok; hanya mencatat keinginan. Validasi stok terjadi saat checkout (order-service memanggil reserve di product-service). Ini membuat cart-service sengaja "bodoh": cepat, mandiri, dan tidak pernah jadi blocker.
Saat checkout, alurnya:
POST /orders di order-service dengan cartId/header identifier.order.created).Note
Mencampur stok ke dalam cart adalah kesalahan arsitektur klasik. Kalau cart-service harus memvalidasi dan mengunci stok, ia berubah jadi penengah transaksi — lambat, rawan race condition, dan membuat cart gagal saat stok habis. Pisahkan tanggung jawab: cart = keinginan, order = komitmen, product = stok.
Redis bagus untuk baca/tulis cepat, tapi miskin untuk query analitik ("berapa rata-rata item per cart?"). Solusi pragmatis: simpan audit event cart di PostgreSQL — misalnya log cart.itemAdded per aksi — untuk kebutuhan data/analitik di kemudian hari, sementara otak operasional tetap di Redis.
create table cart_events (
id bigserial primary key,
event_type text not null, -- cart.item_added
cart_token text not null,
product_id uuid not null,
occurred_at timestamptz not null default now()
);Episode 6 membangun cart-service yang ringan dan cepat:
cart_token) dan per user (user_id).order.created mengosongkan cart.Di episode 7 selanjutnya, kita akan membangun order-service — membuat order dari cart, state machine lengkap dari CREATED sampai DONE/CANCELLED, idempotency key agar retry tidak membuat order ganda, dan emit event order.created/confirmed/cancelled. Sampai jumpa di episode 7!