Portal/Notes 📝
Design system

Astro Q-Commerce — System Design Case Interview

11 case study system design untuk Astro (q-commerce / instant grocery) + bonus perbandingan Klik Indomaret: inventory real-time per dark store, geo-serviceability, driver dispatch, ETA estimation, checkout saga, promo engine, search catalog, real-time tracking, demand forecasting, dan dynamic pricing. Mencakup implementasi Go, trade-off, edge case, dan potential follow-up question per case.

Model bisnis: q-commerce / instant grocery — dark store (micro-fulfillment center), antar dalam hitungan menit, 15k+ SKU, 24 jam, armada driver sendiri ("Astronauts"), area Jabodetabek, bayar e-wallet/VA (no COD).

Case interview muncul dari constraint khas q-commerce: inventory real-time per hub, dispatch driver, geo-serviceability, dan latency rendah. Flash sale cuma satu dari banyak.


Bonus: Klik Indomaret — Q-Commerce dari Raksasa Ritel

Selain Astro, pemain q-commerce besar di Indonesia: Klik Indomaret dari Indomaret Group. Ini adalah platform instant delivery yang memanfaatkan 20k+ toko Indomaret existing sebagai fulfillment point — bukan dark store dedicated seperti Astro. Klik Indomaret punya armada driver sendiri (tidak bergantung Gojek/Grab), menjadikannya perbandingan menarik dari sisi driver dispatch & fleet management.

Perbandingan Model: Astro vs Klik Indomaret

DimensiAstroKlik Indomaret
FulfillmentDark store (micro-fulfillment) dedicated20k+ toko Indomaret existing
DeliveryDriver sendiri ("Astronauts")Driver sendiri (fleet internal)
SKU15k+ (grocery focused)Produk Indomaret (snack, sembako, obat)
CoverageJabodetabek (terbatas)Nasional (seluruh toko Indomaret)
InventoryReal-time per dark store, stok dalamReal-time per toko kecil, stok terbatas
ETA Target15-30 menit1-2 jam (jarak toko→rumah)
Model BisnisInstant grocery, margin dari efisiensiOmnichannel retail, margin dari skala

Aspek Teknis yang Membedakan

Store Selection — Cari Toko Terdekat dengan Stok:

Berbeda dengan Astro yang pre-assign user ke hub via polygon, Klik Indomaret harus dynamic store selection tiap kali user order: cari toko terdekat yang punya SEMUA item di cart. Ini lebih kompleks karena stok per toko kecil dan volatile.

// StoreSelector: cari toko terdekat dengan semua item tersedia (Go)
package klikindomaret

import (
    "context"
    "fmt"
    "sort"

    "github.com/redis/go-redis/v9"
    "github.com/uber/h3-go/v4"
)

type Store struct {
    ID       string
    Lat, Lng float64
    H3Cell   string // H3 resolution 9 (~174m²)
}

type StoreSelector struct {
    rdb     *redis.ClusterClient
    stores  []Store // preload via GeoIndex
}

// FindBestStore: cari toko terdekat yang punya SEMUA item dengan qty cukup.
// Strategi: iterasi toko dari terdekat, check stok via Redis pipeline.
func (ss *StoreSelector) FindBestStore(
    ctx context.Context,
    userLat, userLng float64,
    items []CartItem,
) (*Store, error) {
    // 1. Sort toko by haversine distance
    candidates := ss.nearestStores(userLat, userLng)
    
    // 2. Cek stok di tiap toko dengan Redis pipeline (batch check)
    for _, store := range candidates {
        if ss.hasAllStock(ctx, store.ID, items) {
            return &store, nil
        }
    }
    return nil, fmt.Errorf("no store with all items in stock")
}

func (ss *StoreSelector) hasAllStock(
    ctx context.Context, storeID string, items []CartItem,
) bool {
    pipe := ss.rdb.Pipeline()
    cmds := make([]*redis.StringCmd, len(items))
    
    for i, item := range items {
        key := fmt.Sprintf("stock:%s:%s", storeID, item.SkuID)
        cmds[i] = pipe.Get(ctx, key)
    }
    
    if _, err := pipe.Exec(ctx); err != nil {
        return false
    }
    
    for i, cmd := range cmds {
        stock, err := cmd.Int64()
        if err != nil || stock < int64(items[i].Qty) {
            return false
        }
    }
    return true
}

func (ss *StoreSelector) nearestStores(lat, lng float64) []Store {
    sort.Slice(ss.stores, func(i, j int) bool {
        return haversine(lat, lng, ss.stores[i].Lat, ss.stores[i].Lng) <
               haversine(lat, lng, ss.stores[j].Lat, ss.stores[j].Lng)
    })
    // Return top 5 terdekat (stok per toko kecil, mungkin perlu beberapa percobaan)
    if len(ss.stores) > 5 {
        return ss.stores[:5]
    }
    return ss.stores
}

Driver Dispatch — Fleet Management Skala Nasional:

Tantangan beda dengan Astro: driver Klik Indomaret tersebar di 20k+ toko, bukan terkonsentrasi di beberapa dark store. Dispatch harus mempertimbangkan:

  • Driver terdekat dengan toko yang dipilih (bukan ke user dulu — driver pickup dari toko)
  • Driver bisa assigned ke toko berbeda tiap order
  • Utilisasi driver di area dengan kepadatan order rendah
// DriverDispatcher: match driver ke toko + order (Go)
type DriverDispatcher struct {
    rdb       *redis.ClusterClient
    geoIndex  *GeoIndex            // driver locations via GEOSEARCH
}

var dispatchScript = redis.NewScript(`
    -- Atomic claim: cek driver masih idle + assign order
    local driverKey = KEYS[1]   -- driver:{id}
    local orderKey  = KEYS[2]   -- order:{id}
    local storeID   = ARGV[1]
    local orderID   = ARGV[2]
    local ttl       = tonumber(ARGV[3])  -- accept window (30 detik)

    local status = redis.call('HGET', driverKey, 'status')
    if status ~= 'idle' then
        return {0, 'driver_not_available'}
    end

    -- Atomic: claim driver + assign order
    redis.call('HSET', driverKey,
        'status', 'dispatched',
        'order_id', orderID,
        'store_id', storeID,
        'dispatched_at', ARGV[4])
    redis.call('EXPIRE', driverKey, ttl)  -- auto-release kalau driver ga accept

    redis.call('HSET', orderKey,
        'status', 'driver_assigned',
        'driver_id', KEYS[1],
        'store_id', storeID)

    return {1, 'ok'}
`)

func (dd *DriverDispatcher) Dispatch(
    ctx context.Context, orderID, storeID string, storeLat, storeLng float64,
) (*Driver, error) {
    // 1. Cari driver idle terdekat dari TOKO (bukan dari user)
    drivers, err := dd.geoIndex.NearestIdle(ctx, storeLat, storeLng, 5)
    if err != nil || len(drivers) == 0 {
        return nil, fmt.Errorf("no idle driver near store")
    }

    // 2. Coba claim driver satu per satu
    now := time.Now().Unix()
    for _, d := range drivers {
        res, err := dispatchScript.Run(ctx, dd.rdb,
            []string{
                fmt.Sprintf("driver:%s", d.ID),
                fmt.Sprintf("order:%s", orderID),
            },
            storeID, orderID, 30, now,
        ).Slice()
        if err != nil {
            continue
        }
        if res[0].(int64) == 1 {
            return &d, nil
        }
    }
    return nil, fmt.Errorf("all nearby drivers busy")
}

Relevansi dengan Case Interview

Case existing bisa di-extend dengan perspektif Klik Indomaret:

  • #2 Inventory: beda fundamental — Astro stok per dark store (dalam, SKU banyak), Klik Indomaret stok per toko kecil (dangkal, SKU sedikit). Risk out-of-stock jauh lebih tinggi → perlu fallback store + partial fulfillment strategy
  • #3 Geo Serviceability: Astro pakai polygon (static), Klik Indomaret pakai dynamic store selection (real-time). Interviewer suka tanya: "kapan pakai static vs dynamic assignment?"
  • #4 Driver Dispatch: skala nasional 20k toko vs Astro 50 dark store Jabodetabek. Fleet management完全不同 — perlu zone-based dispatch + load balancing antar kecamatan
  • #10 Demand Forecasting: stok per toko kecil bikin forecast lebih noise. Butuh aggregate forecast per kecamatan → disaggregate ke toko berdasarkan historical demand proporsi

1. Flash Sale — High-Concurrency Timed Event

Baca detail & code →

Masalah: event dengan waktu terbatas (misal 12:00–14:00), stok terbatas per SKU per hub, ribuan user rebutan dalam hitungan detik. Ini ujian ekstrim untuk hot key contention, rate limiting, fairness, dan anti-bot.

Inti Desain

Stock partitioning (hot key mitigation):

SKU laris (misal 100 unit telur promo) dipecah jadi N bucket (misal 10 bucket @10 unit). Key Redis: flash:stock:{event_id}:{store_id}:{sku_id}:{bucket_N}. Saat checkout, pilih bucket random — contention terdistribusi ke N key. Kalau bucket habis, coba bucket lain. Semua bucket habis → sold out.

// StockPartitioner: atomic decrement dengan bucket fallback (Go + go-redis)
package flashsale

import (
    "context"
    "fmt"

    "github.com/redis/go-redis/v9"
)

var ErrSoldOut = fmt.Errorf("sold out")

// Lua script — dieksekusi atomic oleh Redis server-side
var stockDecrementScript = redis.NewScript(`
    local buckets = KEYS
    local qty = tonumber(ARGV[1])
    for i = 1, #buckets do
        local bucket = buckets[math.random(#buckets)]
        local stock = redis.call('GET', bucket)
        if stock and tonumber(stock) >= qty then
            redis.call('DECRBY', bucket, qty)
            return {1, bucket}
        end
    end
    return {0, nil}
`)

type StockPartitioner struct {
    rdb *redis.ClusterClient
}

func NewStockPartitioner(rdb *redis.ClusterClient) *StockPartitioner {
    return &StockPartitioner{rdb: rdb}
}

// ReserveStock: atomic decrement dengan random bucket selection.
// Return bucket key yang berhasil, atau ErrSoldOut.
func (s *StockPartitioner) ReserveStock(
    ctx context.Context,
    eventID, storeID, skuID string,
    totalBuckets, qty int,
) (string, error) {
    keys := make([]string, totalBuckets)
    for i := range totalBuckets {
        keys[i] = fmt.Sprintf(
            "flash:stock:%s:%s:%s:bucket_%d",
            eventID, storeID, skuID, i,
        )
    }

    res, err := stockDecrementScript.Run(ctx, s.rdb, keys, qty).Slice()
    if err != nil {
        return "", fmt.Errorf("redis eval: %w", err)
    }
    if res[0].(int64) == 0 {
        return "", ErrSoldOut
    }
    return res[1].(string), nil
}

// FlashSaleService: orchestrator untuk checkout flash sale.
type FlashSaleService struct {
    partitioner *StockPartitioner
    limiter     *RateLimiter         // sliding window per device fingerprint
    queue       *VirtualQueue        // waiting room (Redis sorted set)
    producer    kafka.Producer       // outbox event
}

func (svc *FlashSaleService) Checkout(
    ctx context.Context, req CheckoutRequest,
) (*CheckoutResponse, error) {
    // 1. Rate limit check — device fingerprint + IP + session
    if !svc.limiter.Allow(ctx, req.DeviceFingerprint) {
        // Masukkan ke waiting room
        pos, eta := svc.queue.Enqueue(ctx, req)
        return &CheckoutResponse{
            Status:   "waiting",
            Position: pos,
            ETA:      eta,
        }, nil
    }

    // 2. Atomic stock reservation dengan bucket partitioning
    bucket, err := svc.partitioner.ReserveStock(
        ctx, req.EventID, req.StoreID, req.SkuID, 10, req.Qty,
    )
    if errors.Is(err, ErrSoldOut) {
        svc.queue.KickAll(ctx, req.EventID) // notifikasi semua waiting
        return &CheckoutResponse{Status: "sold_out"}, nil
    }
    if err != nil {
        return nil, fmt.Errorf("reserve stock: %w", err)
    }

    // 3. Produce order event → async processing (Kafka outbox)
    svc.producer.Produce(ctx, OrderEvent{
        EventID: req.EventID,
        Bucket:  bucket,
        Qty:     req.Qty,
        UserID:  req.UserID,
    })

    return &CheckoutResponse{Status: "success", Bucket: bucket}, nil
}

Waiting room / virtual queue:

Kalau traffic jauh di atas kapasitas (misal 50k user rebut 500 unit), pakai queue system:

  • User masuk waiting room sebelum bisa akses halaman flash sale
  • Queue FIFO dengan token-based admission — hanya N user yang boleh masuk checkout bersamaan
  • Tampilkan posisi antrian + estimasi waktu
  • Kalau stok habis, kick semua dari queue sekaligus — UX: "Maaf, flash sale sudah berakhir"

Rate limiting per user:

  • Token bucket / sliding window di API gateway — cegah 1 user spam puluhan checkout request paralel
  • Fingerprint: device ID + IP + session → deteksi multi-akun dari device yang sama
  • Checkout hanya boleh 1x per user per flash sale event (idempotency key: {user_id}:{event_id})

Cache strategy:

  • Halaman produk flash sale di-cache di CDN dengan TTL pendek (1–5 detik) + stale-while-revalidate
  • Stock count di halaman pakai Redis langsung, bukan dari cache
  • Jangan cache penuh — stok "tinggal 3!" harus real-time, bukan basi 5 detik

Arsitektur

User → CDN (static+stale) → API Gateway (rate limit)
  → Flash Sale Service
    → Redis Cluster (stock reservation, EVALSHA via Go)
    → Kafka (order event, async processing)
    → Postgres (system of record, async sync)

Service terpisah dari catalog/checkout reguler — isolasi blast radius. Redis cluster dedicated untuk flash sale, jangan share sama session cache atau catalog cache.

Edge & Risk

  • Race antar bucket: user A dan B sama-sama lihat stok tersedia di bucket yang sama → Lua script handle atomicity, tapi UX "stok habis saat checkout" masih bisa terjadi (bucket habis pas concurrent decrement). Wajib handle graceful.
  • Bot/scalper: user pakai script otomatis bypass rate limit → wajib CAPTCHA/device attestation di entry flash sale. Lebih penting dari e-commerce biasa karena margin kecil + volume.
  • Cache stampede: tepat saat flash sale mulai, puluhan ribu request ke origin → solusi: pre-warm cache sebelum event mulai, jangan expire cache pas jam 12:00:00.
  • Stock reconciliation: Redis crash saat flash sale → recovery dari Postgres, tapi ada window inconsistency. Bisa pakai Redis AOF persistence + sentinel.
  • Phantom stock: bucket habis di Redis tapi Postgres belum di-update, lalu Redis restart → stok "muncul" lagi. Wajib source of truth yang jelas (Redis = operational, Postgres = authoritative).

Potential Follow-up

  • "Kenapa stock partitioning bukan sharding by store?"
  • "Gimana cara deteksi dan block scalper real-time tanpa ganggu user genuine?"
  • "Apa trade-off waiting room vs langsung checkout?"
  • "Bagaimana kalau Redis master mati pas flash sale lagi jalan? Gimana failover-nya?"
  • "Berapa banyak bucket yang optimal? Gimana cara nentuinnya?"
  • "Kenapa service terpisah? Kenapa tidak pakai checkout reguler yang sama?"

2. Real-time Inventory per Dark Store (Oversell Prevention)

Baca detail & code →

Ini jantung q-commerce.

Masalah: stok fisik ada di banyak dark store. User di Tebet cuma boleh lihat & beli stok dari hub yang cover Tebet. Dua user rebutan 1 unit terakhir telur — jangan sampai dua-duanya berhasil checkout (oversell), karena fulfillment fisik bakal gagal dan itu pengalaman buruk + refund.

Inti Desain

  • Stok bersifat per-hub, jadi key-nya (store_id, sku_id), bukan global. Sharding natural by store.
  • Pisahkan available vs on-hand. Saat add-to-cart/checkout lakukan reservation (decrement available, TTL misal 10–15 menit), bukan langsung potong on-hand. Kalau bayar gagal/timeout, reservation balik (compensating).
  • Atomicity: pakai Redis DECRBY dengan Lua script (cek-lalu-kurang dalam satu atomic op) sebagai source of truth untuk available, lalu async sync ke Postgres sebagai system of record. Atau full Postgres dengan UPDATE ... WHERE qty >= n (optimistic, andalkan row lock + affected-rows check). Trade-off: Redis cepat tapi butuh rekonsiliasi; Postgres konsisten tapi row contention tinggi di SKU laris.

Edge & Risk

  • Reservation leak kalau service crash sebelum release → wajib TTL + reaper job.
  • Hot key (SKU promo) → contention di satu row/key. Mitigasi: stock partitioning (pecah 100 unit jadi 10 bucket @10, decrement random bucket).
  • Konsistensi stok fisik vs sistem (barang rusak, hilang, salah hitung) → butuh cycle counting & adjustment flow, sama event audit trail.

Potential Follow-up

  • "Bagaimana kalau Redis mati? Gimana recovery-nya?"
  • "Gimana cara reconcile Redis dan Postgres kalau ada drift?"
  • "Apa trade-off reservation TTL pendek vs panjang?"

3. Serviceability & Hub Assignment (Geo)

Baca detail & code →

Masalah: user buka app, sistem harus tentukan dia masuk coverage hub mana, dalam <100ms, sebelum render katalog+harga+stok.

Inti Desain

  • Tiap dark store punya service polygon (bukan cuma radius, karena jalan/sungai/tol bikin radius bohong). Simpan polygon di PostGIS, query ST_Contains(polygon, user_point).
  • Untuk skala & latency: precompute pakai geohash / H3 (Uber) → map cell → store. Lookup jadi O(1) by cell index di Redis, fallback ke PostGIS kalau cell di boundary.
  • Kalau satu titik di-cover >1 hub → tie-break by jarak/ETA/ketersediaan stok/load hub.

Trade-off

H3 cepat tapi cell di tepi polygon bisa false positive/negative → perlu resolusi cell cukup halus + verifikasi PostGIS di edge case.

Risk

User di luar coverage harus dapat UX jelas (waitlist/daftar area), bukan katalog kosong.

Potential Follow-up

  • "Kenapa H3 bukan S2? Apa bedanya?"
  • "Gimana handle perubahan polygon coverage (hub baru, hub tutup)?"
  • "Bagaimana kalau user di perbatasan dua hub dan stok di hub A ada, di hub B habis?"

4. Order Dispatch & Driver Allocation

Baca detail & code →

Masalah: order masuk → assign ke driver yang tepat. Tujuan: minimize delivery time, maximize utilisasi driver, fair distribution.

Inti Desain

  • Ini bukan FIFO sederhana, ini assignment problem. Pendekatan batching: kumpulkan order beberapa detik, jalankan matching (Hungarian/greedy) terhadap driver idle, pertimbangkan jarak hub→tujuan, arah, beban, ETA siap-pickup.
  • State driver di-track real-time (lokasi, status: idle/picking/delivering). Simpan di Redis + geo-index (GEOADD/GEOSEARCH) buat cari driver terdekat dari hub.
  • Batch delivery (multi-order satu driver) kalau searah → naikin throughput tapi naikin ETA order tertentu. Trade-off eksplisit yang interviewer suka denger.

Edge

Tidak ada driver available (jam sibuk) → queue + dynamic ETA + surge. Driver reject/cancel → re-assign cepat tanpa starvation order.

Potential Follow-up

  • "Gimana cara deteksi driver yang sengaja lambat/mangkir?"
  • "Bagaimana kalau driver lagi dalam perjalanan, tiba-tiba ada order baru yang lebih optimal?"
  • "Apa bedanya assignment problem ini dengan ride-hailing (Gojek/Grab)?"

5. ETA Estimation ("tiba dalam X menit")

Baca detail & code →

Janji utama Astro. ETA = waktu picking di hub + antrian dispatch + travel time.

Inti Desain

  • Travel time bukan haversine/jarak lurus — pakai routing engine (OSRM/Google) atau model ML berbasis histori (traffic, jam, cuaca, hari).
  • Picking time tergantung jumlah item & beban hub saat itu.
  • Tampilkan ETA konservatif (under-promise) karena miss ETA = trust rusak. Ini keputusan produk yang punya konsekuensi engineering (buffer, percentile p80 bukan rata-rata).

Risk

ETA dipakai juga buat dispatch decision → kalau model bias, dispatch ikut salah.

Potential Follow-up

  • "Gimana cara validasi akurasi ETA model?"
  • "Apa metric yang dipakai untuk monitor ETA miss?"
  • "Bagaimana handle ETA kalau driver tiba-tiba cancel di tengah jalan?"

6. Checkout, Payment & Distributed Transaction

Baca detail & code →

Masalah: checkout menyentuh banyak service — reserve stok, buat order, charge payment (e-wallet/VA via PG eksternal), assign hub. Harus konsisten walau ada partial failure.

Inti Desain

  • Saga pattern (orchestration lebih gampang di-trace daripada choreography): reserve stok → create order (PENDING) → call PG → on success confirm; on fail/timeout → compensate (release stok, cancel order).
  • Idempotency wajib: payment callback dari PG bisa dobel/out-of-order. Pakai idempotency key di create-order & di handler callback. Webhook PG harus idempotent dan signature-verified.
  • Outbox pattern buat publish event order (jangan dual-write DB + broker tanpa outbox → bisa lost/ghost event).

Risk/Security

Webhook spoofing → verifikasi signature; race antara user-cancel dan payment-success; double-charge.

Potential Follow-up

  • "Kenapa orchestration bukan choreography?"
  • "Gimana cara test saga rollback secara end-to-end?"
  • "Apa yang terjadi kalau payment gateway timeout tapi di sisi mereka sukses?"
  • "Bagaimana handle refund flow?"

7. Promo / Voucher Engine

Baca detail & code →

Astro punya aturan promo non-trivial: gratis ongkir 2x pertama, min Rp100k, potongan Rp5k. Ini case rules + anti-abuse.

Inti Desain

  • Rule engine: kondisi (min transaksi, first-N-order, area, kategori) → efek (discount, free ongkir). Stackable atau eksklusif? Urutan apply? Ini detail yang ditanya.
  • Counter "berapa kali user sudah pakai" harus atomic & race-safe (user spam checkout paralel buat dobel promo).
  • Anti-fraud: multi-akun by device, nomor HP, alamat, pembayaran. Promo "first order" dimanfaatin dengan akun baru terus.

Risk

Budget promo bisa jebol kalau ga ada cap; voucher race bisa over-redeem.

Potential Follow-up

  • "Gimana cara desain rule engine yang bisa dikonfigurasi tim produk tanpa deploy?"
  • "Apa perbedaan voucher-based vs automatic promo?"
  • "Bagaimana deteksi abuse tanpa bikin false positive yang ganggu user genuine?"

8. Search & Catalog (15k SKU)

Baca detail & code →

Masalah: search cepat + relevan, tapi hanya tampilkan produk yang in-stock di hub user.

Inti Desain

  • Elasticsearch/OpenSearch buat full-text + typo tolerance (telor→telur) + autocomplete.
  • Tantangan: filter availability per-hub. Index per-hub mahal. Pilihan: index katalog global, lalu filter stok dari Redis saat query-time, atau simpan stok flag di index dan update via stream (eventual). Trade-off freshness vs cost.
  • Ranking: relevansi + ketersediaan + margin + popularitas.

Risk

Stok index basi → user lihat produk yang ternyata habis (UX buruk, balik ke masalah #2).

Potential Follow-up

  • "Kenapa Elasticsearch bukan PostgreSQL full-text search?"
  • "Gimana cara handle reindexing tanpa downtime?"
  • "Bagaimana handle search saat traffic spike (jam makan siang/promosi)?"

9. Real-time Order Tracking

Baca detail & code →

Masalah: user lihat driver bergerak di peta real-time.

Inti Desain

  • Driver app kirim lokasi tiap beberapa detik → ingest (bisa via MQTT/WebSocket) → push ke user yang relevan.
  • Jangan broadcast semua; fan-out hanya ke subscriber order itu. Pakai pub/sub keyed by order.
  • Throttle & smoothing posisi; tahan beban dari ribuan driver concurrent.

Trade-off

Frekuensi update vs biaya bandwidth/server.

Potential Follow-up

  • "Kenapa MQTT bukan WebSocket langsung?"
  • "Gimana cara smooth posisi driver di map biar tidak lompat-lompat?"
  • "Apa yang terjadi kalau driver matikan GPS/lokasi?"

10. Demand Forecasting & Replenishment

Baca detail & code →

Lebih ke data/backend tapi sering muncul karena ini efisiensi modalnya.

Masalah: tiap hub harus stok barang yang bener supaya ga out-of-stock (lost sale) tapi ga overstock (fresh produce busuk).

Inti Desain

Forecast per-SKU per-hub (musiman, hari, cuaca, promo) → auto-replenishment order ke central warehouse. Fresh goods butuh handling expiry (FEFO — First Expired First Out).

Nilai plus: kalau pernah implement demand forecasting (Holt-Winters, ARIMA, Prophet) bisa diangkat sebagai pengalaman relevan.

Potential Follow-up

  • "Apa metrik akurasi forecasting yang paling penting?"
  • "Gimana handle cold-start (hub baru, SKU baru, tidak ada histori)?"
  • "Bagaimana beda strategi replenishment untuk dry goods vs fresh produce?"

11. Dynamic Delivery Fee / Surge

Baca detail & code →

Astro patok ongkir Rp15k dengan banyak promo. Case: ongkir dinamis berdasar jarak, beban driver, jam sibuk, cuaca.

Inti Desain

Pricing service ambil signal (supply driver vs demand order di area, jarak, weather) → hitung fee. Harus konsisten antara yang ditampilkan saat checkout dan yang di-charge (lock harga saat checkout, jangan berubah di tengah).

Potential Follow-up

  • "Gimana cara pastiin harga yang dilihat user = harga yang di-charge?"
  • "Apa elasticity demand untuk delivery fee? Gimana cara ukurnya?"
  • "Bagaimana komunikasi perubahan fee ke user tanpa bikin churn?"

Artikel Terkait (News)

Setiap case punya artikel detail dengan Go code, Mermaid diagrams, dan system design lengkap:


Prioritas Belajar (Waktu Terbatas)

  1. #1 Flash Sale — high-concurrency, hot key, stock partitioning, rate limiting, anti-bot
  2. #2 Inventory — jantung q-commerce, oversell prevention + reservation
  3. #4 Dispatch — assignment problem + real-time driver state
  4. #6 Checkout/Saga — distributed transaction + idempotency
  5. #3 Geo — serviceability + hub assignment

Lima ini paling "Astro banget" dan paling membedakan dari e-commerce biasa.


Catatan

Asumsi: role backend/fullstack (Go), interview-nya system design + behavioral, bukan pure algo. Kalau fokusnya ke spesifik lain (pure DSA, frontend, infra/k8s), case-nya berbeda.

Edit on GitHub

Last updated on

Astro Q-Commerce — System Design Case Interview | Faisal Affan