🏭 Domains✍️ Khoa📅 19/04/2026☕ 18 phút đọc

Domain: E-commerce & Marketplace

E-commerce trông có vẻ đơn giản: user click "Mua hàng", hệ thống trừ tồn kho, gửi đơn. Nhưng khi scale lên hàng triệu SKU, 100k RPS trong flash sale, và vấn đề overselling có thể khiến công ty mất hàng tỷ đồng — mọi thứ bắt đầu phức tạp.

Section này mô tả cách các marketplace như Shopee, Lazada, Tiki thiết kế hệ thống để survive flash sale 12.12, prevent overselling, và handle millions of concurrent users mà không crash.


1. Catalog & Product Data Model

1.1 SKU vs SPU

SPU (Standard Product Unit) = Sản phẩm chung
  Ví dụ: iPhone 15 Pro

SKU (Stock Keeping Unit) = Biến thể cụ thể
  Ví dụ: iPhone 15 Pro - 256GB - Titan Tự Nhiên
         iPhone 15 Pro - 512GB - Titan Xanh
  → Mỗi SKU có price, stock, barcode riêng

Schema thực tế:

-- SPU — thông tin chung của sản phẩm
CREATE TABLE products (
    id              BIGSERIAL PRIMARY KEY,
    product_id      VARCHAR(50) UNIQUE NOT NULL,   -- SPU code
    name            TEXT NOT NULL,
    description     TEXT,
    brand_id        BIGINT REFERENCES brands(id),
    category_id     BIGINT REFERENCES categories(id),
    status          VARCHAR(20) NOT NULL,          -- ACTIVE, DELISTED, DRAFT
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- SKU — biến thể cụ thể với attributes
CREATE TABLE product_variants (
    id              BIGSERIAL PRIMARY KEY,
    sku             VARCHAR(50) UNIQUE NOT NULL,
    product_id      BIGINT REFERENCES products(id),
    attributes      JSONB NOT NULL,                -- {color: "blue", size: "256GB"}
    price           BIGINT NOT NULL,               -- đơn vị: đồng
    stock           INT NOT NULL DEFAULT 0,
    weight_grams    INT,                           -- shipping calculation
    is_active       BOOLEAN NOT NULL DEFAULT true,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT chk_price CHECK (price >= 0),
    CONSTRAINT chk_stock CHECK (stock >= 0)
);

-- Index quan trọng để tránh overselling (race condition)
CREATE INDEX idx_sku_stock ON product_variants(sku) WHERE stock > 0;
CREATE INDEX idx_product_category ON products(category_id) WHERE status = 'ACTIVE';

1.2 Catalog Search — Elasticsearch

Khi có 50 triệu SKU, PostgreSQL LIKE '%keyword%' sẽ chết ngay trong flash sale. Cần full-text search engine:

// Elasticsearch document schema
{
  "sku": "IPH15P-256-TBL",
  "product_id": "IPH15P",
  "name": "iPhone 15 Pro Max 256GB Titan Blue",
  "price": 29990000,
  "stock": 150,
  "category": ["electronics", "phones", "iphone"],
  "brand": "Apple",
  "attributes": {
    "color": "Titan Blue",
    "storage": "256GB",
    "screen_size": "6.7 inch"
  },
  "rating_avg": 4.8,
  "sold_count": 12450,
  "boost_score": 1.5,  // manual ranking boost
  "created_at": "2024-09-22T10:00:00Z"
}

Query với ranking:

# Python với elasticsearch-py
search_query = {
    "query": {
        "function_score": {
            "query": {
                "bool": {
                    "must": [
                        {"multi_match": {
                            "query": "iphone 15 pro",
                            "fields": ["name^3", "brand^2", "category"],
                            "type": "best_fields",
                            "fuzziness": "AUTO"
                        }}
                    ],
                    "filter": [
                        {"term": {"is_active": True}},
                        {"range": {"stock": {"gt": 0}}},
                        {"range": {"price": {"gte": 10000000, "lte": 50000000}}}
                    ]
                }
            },
            "functions": [
                {"field_value_factor": {
                    "field": "sold_count",
                    "modifier": "log1p",
                    "factor": 0.1
                }},
                {"field_value_factor": {
                    "field": "rating_avg",
                    "modifier": "sqrt",
                    "factor": 0.5
                }},
                {"gauss": {  # decay score theo thời gian (ưu tiên mới)
                    "created_at": {
                        "origin": "now",
                        "scale": "30d",
                        "decay": 0.5
                    }
                }}
            ],
            "score_mode": "sum",
            "boost_mode": "multiply"
        }
    },
    "size": 20,
    "from": 0
}

Tại sao function_score?

Ranking chỉ dựa BM25 text similarity không đủ — bạn muốn:

  • Sản phẩm bán chạy (sold_count) càng cao càng lên đầu
  • Rating tốt boost score
  • Sản phẩm mới hơn được ưu tiên hơn hàng cũ
  • Merchant có thể boost thủ công (ads placement)

2. Inventory Management — Overselling Problem

2.1 Vấn đề Race Condition

Flash Sale: Còn 1 sản phẩm cuối cùng, 2 user click cùng lúc

User A                     Database                     User B
  │                            │                           │
  │─ SELECT stock─────────────►│                           │
  │◄─ stock=1 ─────────────────│                           │
  │                            │◄───────── SELECT stock ───│
  │                            │─ stock=1 ─────────────────►│
  │─ UPDATE stock=0 ───────────►│                           │
  │                            │◄───────── UPDATE stock=0 ─│
  └─ Success                   │                           └─ Success
                  
  Kết quả: Bán 2 sản phẩm nhưng chỉ có 1 → OVERSELLING!

2.2 Giải pháp: Pessimistic Locking

BEGIN;

-- Lock row trong khi transaction chưa commit
SELECT stock
FROM product_variants
WHERE sku = 'IPH15P-256-TBL'
FOR UPDATE;  -- ← Pessimistic lock: User B phải đợi User A commit

-- Check stock trước khi trừ
IF stock < requested_quantity THEN
    ROLLBACK;
    RAISE EXCEPTION 'insufficient_stock';
END IF;

-- Update stock
UPDATE product_variants
SET stock = stock - requested_quantity,
    updated_at = now()
WHERE sku = 'IPH15P-256-TBL';

-- Tạo order
INSERT INTO orders (user_id, sku, quantity, price) 
VALUES (...);

COMMIT;

Trade-off: Pessimistic lock đảm bảo data consistency nhưng giảm throughput → Trong flash sale với 100k RPS, lock contention sẽ làm chậm hệ thống.

2.3 Giải pháp: Optimistic Locking với Version

CREATE TABLE product_variants (
    ...
    stock       INT NOT NULL,
    version     BIGINT NOT NULL DEFAULT 0,  -- ← Version field
    ...
);

-- Go code
func DeductStock(db *sql.DB, sku string, qty int) error {
    tx, _ := db.Begin()
    defer tx.Rollback()

    var currentStock, version int64
    err := tx.QueryRow(`
        SELECT stock, version
        FROM product_variants
        WHERE sku = $1
    `, sku).Scan(&currentStock, &version)
    if err != nil {
        return err
    }

    if currentStock < int64(qty) {
        return ErrInsufficientStock
    }

    // Update chỉ thành công nếu version không thay đổi
    result, err := tx.Exec(`
        UPDATE product_variants
        SET stock = stock - $1,
            version = version + 1,
            updated_at = now()
        WHERE sku = $2
          AND version = $3  -- ← Optimistic lock check
    `, qty, sku, version)
    if err != nil {
        return err
    }

    rowsAffected, _ := result.RowsAffected()
    if rowsAffected == 0 {
        return ErrVersionMismatch // retry by client
    }

    return tx.Commit()
}

Client retry khi gặp ErrVersionMismatch với exponential backoff.

2.4 Giải pháp Flash Sale: Pre-reserve Inventory

Thay vì trừ stock trực tiếp DB mỗi lần click:

1. Pre-load hot SKUs vào Redis khi flash sale bắt đầu
   
   HSET flash:IPH15P-256  stock 1000
   HSET flash:IPH15P-256  price 25990000

2. User click "Mua" → Trừ stock từ Redis (atomic DECR)
   
   stock_left = DECR flash:IPH15P-256:stock
   IF stock_left < 0:
       INCR flash:IPH15P-256:stock  -- rollback
       RETURN "sold_out"

3. Background job sync Redis → PostgreSQL mỗi 10s
   (eventual consistency acceptable trong flash sale)

4. Sau flash sale, reconcile Redis vs DB
func ReserveFlashSaleStock(ctx context.Context, rdb *redis.Client, sku string, qty int) error {
    script := `
        local stock = redis.call('HGET', KEYS[1], 'stock')
        if stock == false then
            return -1  -- key not found
        end
        
        stock = tonumber(stock)
        if stock < tonumber(ARGV[1]) then
            return 0  -- insufficient
        end
        
        redis.call('HINCRBY', KEYS[1], 'stock', -tonumber(ARGV[1]))
        return 1  -- success
    `
    
    result, err := rdb.Eval(ctx, script, []string{"flash:" + sku}, qty).Int()
    if err != nil {
        return err
    }
    
    switch result {
    case -1:
        return ErrFlashSaleNotFound
    case 0:
        return ErrSoldOut
    default:
        return nil
    }
}

Trade-off: Eventual consistency — nếu Redis crash trước khi sync, có thể mất sync. Nhưng chấp nhận được vì:

  • Flash sale chỉ kéo dài vài phút
  • Redis persistence (AOF) giảm thiểu risk
  • User experience tốt hơn nhiều (latency thấp, no DB bottleneck)

3. Order Management — State Machine

3.1 Order States

Order Lifecycle:

 PENDING ────────► CONFIRMED ──────► PROCESSING ──────► SHIPPED ──────► DELIVERED
    │                   │                                                    │
    │                   │                                                    ▼
    │                   │                                               COMPLETED
    │                   │
    │                   ▼ (payment timeout 15 mins)
    └──────────────► CANCELLED
                        ▲
                        │
                        │ (user cancel before ship)
                        │
            ┌───────────┴────────────────┐
      CONFIRMED              PROCESSING

State transitions:
  PENDING → CONFIRMED:    payment success
  PENDING → CANCELLED:    payment timeout (TTL 15 mins)
  CONFIRMED → PROCESSING: merchant pack hàng
  CONFIRMED → CANCELLED:  user hoặc merchant cancel
  PROCESSING → SHIPPED:   handover to shipper
  SHIPPED → DELIVERED:    GPS confirm delivered
  DELIVERED → COMPLETED:  không return trong 7 ngày

Schema:

CREATE TABLE orders (
    id              BIGSERIAL PRIMARY KEY,
    order_no        VARCHAR(20) UNIQUE NOT NULL,  -- ORD20240412-XXXXXX
    user_id         BIGINT NOT NULL,
    merchant_id     BIGINT NOT NULL,
    status          VARCHAR(20) NOT NULL,
    subtotal        BIGINT NOT NULL,
    shipping_fee    BIGINT NOT NULL DEFAULT 0,
    discount        BIGINT NOT NULL DEFAULT 0,
    total           BIGINT NOT NULL,
    paid_at         TIMESTAMPTZ,
    expired_at      TIMESTAMPTZ,                 -- payment deadline
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT chk_total CHECK (total = subtotal + shipping_fee - discount),
    CONSTRAINT chk_subtotal CHECK (subtotal >= 0)
);

CREATE TABLE order_items (
    id              BIGSERIAL PRIMARY KEY,
    order_id        BIGINT REFERENCES orders(id),
    sku             VARCHAR(50) NOT NULL,
    product_name    TEXT NOT NULL,              -- snapshot để tránh product đổi tên sau
    quantity        INT NOT NULL,
    unit_price      BIGINT NOT NULL,            -- snapshot giá tại thời điểm mua
    subtotal        BIGINT NOT NULL,
    CONSTRAINT chk_qty CHECK (quantity > 0),
    CONSTRAINT chk_subtotal CHECK (subtotal = quantity * unit_price)
);

CREATE TABLE order_status_history (
    id              BIGSERIAL PRIMARY KEY,
    order_id        BIGINT REFERENCES orders(id),
    from_status     VARCHAR(20),
    to_status       VARCHAR(20) NOT NULL,
    reason          TEXT,
    actor_id        BIGINT,                     -- user/merchant/system
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

3.2 Order Creation — Saga Pattern

Khi user click "Đặt hàng":

1. Validate cart (giá, stock availability)
2. Reserve inventory (trừ stock tạm thời)
3. Calculate shipping fee (gọi 3PL API)
4. Apply discount/voucher (check validity, usage limit)
5. Create order record
6. Call payment gateway (MoMo, ZaloPay, VNPay)
7. Wait payment callback
8. Confirm order & finalize inventory deduction

Nếu step nào fail → compensate các steps trước đó (Saga pattern):
type OrderSaga struct {
    db          *sql.DB
    inventory   InventoryService
    shipping    ShippingService
    payment     PaymentService
    voucher     VoucherService
}

func (s *OrderSaga) CreateOrder(ctx context.Context, req CreateOrderRequest) (*Order, error) {
    sagaID := uuid.New()
    
    // Step 1: Reserve inventory
    reservationID, err := s.inventory.Reserve(ctx, req.Items)
    if err != nil {
        return nil, err
    }
    defer func() {
        if err != nil {
            s.inventory.ReleaseReservation(ctx, reservationID) // compensate
        }
    }()
    
    // Step 2: Calculate shipping
    shippingFee, err := s.shipping.Calculate(ctx, req.Address, req.Items)
    if err != nil {
        return nil, err
    }
    
    // Step 3: Apply voucher
    var discount int64
    if req.VoucherCode != "" {
        discount, err = s.voucher.Apply(ctx, req.UserID, req.VoucherCode, req.Subtotal)
        if err != nil {
            return nil, err
        }
        defer func() {
            if err != nil {
                s.voucher.Release(ctx, req.UserID, req.VoucherCode) // compensate
            }
        }()
    }
    
    // Step 4: Create pending order
    order := &Order{
        OrderNo:     generateOrderNo(),
        UserID:      req.UserID,
        Status:      "PENDING",
        Subtotal:    req.Subtotal,
        ShippingFee: shippingFee,
        Discount:    discount,
        Total:       req.Subtotal + shippingFee - discount,
        ExpiredAt:   time.Now().Add(15 * time.Minute),
    }
    
    if err := s.db.Insert(ctx, order); err != nil {
        return nil, err
    }
    
    // Step 5: Initiate payment
    paymentURL, err := s.payment.CreatePaymentLink(ctx, PaymentRequest{
        OrderNo:     order.OrderNo,
        Amount:      order.Total,
        CallbackURL: fmt.Sprintf("https://api.shop.com/payment/callback/%s", order.OrderNo),
    })
    if err != nil {
        return nil, err
    }
    
    order.PaymentURL = paymentURL
    return order, nil
}

// Payment callback handler (webhook từ MoMo/ZaloPay)
func (s *OrderSaga) HandlePaymentCallback(ctx context.Context, callback PaymentCallback) error {
    order, err := s.db.GetOrderByNo(ctx, callback.OrderNo)
    if err != nil {
        return err
    }
    
    if callback.Status == "SUCCESS" {
        // Confirm order & finalize inventory
        if err := s.inventory.ConfirmReservation(ctx, order.ReservationID); err != nil {
            // Critical error: payment succeeded but inventory confirm failed
            // → Manual intervention hoặc auto-refund
            s.alertOncall(ctx, "payment_inventory_mismatch", order.OrderNo)
            return err
        }
        
        return s.db.UpdateOrderStatus(ctx, order.ID, "CONFIRMED")
    } else {
        // Payment failed/cancelled → release reservation
        s.inventory.ReleaseReservation(ctx, order.ReservationID)
        return s.db.UpdateOrderStatus(ctx, order.ID, "CANCELLED")
    }
}

4. Pricing Strategy & Dynamic Pricing

4.1 Price Types

Base Price:        Giá gốc của merchant set
List Price:        Giá hiển thị (có thể = base hoặc có markup)
Sale Price:        Giá khuyến mại (flash sale, voucher)
Final Price:       Giá user thực trả = Sale Price - Voucher + Shipping

4.2 Dynamic Pricing (Surge Pricing)

Uber-style pricing khi demand cao:

def calculate_dynamic_price(sku: str, base_price: int) -> int:
    """
    Tăng giá khi:
    - Traffic cao (nhiều người đang xem sản phẩm này)
    - Stock thấp (khan hiếm)
    - Competitor bán hết (monopoly tạm thời)
    """
    # Fetch real-time metrics
    concurrent_viewers = redis.get(f"viewers:{sku}") or 0
    stock_left = redis.hget(f"inventory:{sku}", "stock") or 0
    competitor_stock = fetch_competitor_stock(sku)  # crawl data
    
    surge_multiplier = 1.0
    
    # Traffic surge
    if concurrent_viewers > 1000:
        surge_multiplier *= 1.1
    if concurrent_viewers > 5000:
        surge_multiplier *= 1.2
    
    # Low stock
    if stock_left < 10:
        surge_multiplier *= 1.15
    
    # Competitor out of stock
    if competitor_stock == 0:
        surge_multiplier *= 1.25
    
    # Cap at 150% base price (tránh PR disaster)
    surge_multiplier = min(surge_multiplier, 1.5)
    
    return int(base_price * surge_multiplier)

Lưu ý: Dynamic pricing rất nhạy cảm về PR. Shopee/Lazada thường không dùng aggressive surge pricing như Uber/Grab, thay vào đó dùng "flash deal" (giảm giá có thời hạn) để tạo urgency mà không bị backlash.

4.3 Voucher & Promotion Engine

CREATE TABLE vouchers (
    id                  BIGSERIAL PRIMARY KEY,
    code                VARCHAR(20) UNIQUE NOT NULL,
    type                VARCHAR(20) NOT NULL,  -- PERCENTAGE, FIXED_AMOUNT, FREE_SHIP
    value               BIGINT NOT NULL,       -- 10 (10%) hoặc 50000 (50k VND)
    min_order_value     BIGINT NOT NULL DEFAULT 0,
    max_discount        BIGINT,                -- cap discount (10% max 100k)
    max_usage_per_user  INT DEFAULT 1,
    total_usage_limit   INT,                   -- tổng số lượt dùng toàn platform
    valid_from          TIMESTAMPTZ NOT NULL,
    valid_until         TIMESTAMPTZ NOT NULL,
    is_active           BOOLEAN NOT NULL DEFAULT true,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE voucher_usages (
    id          BIGSERIAL PRIMARY KEY,
    voucher_id  BIGINT REFERENCES vouchers(id),
    user_id     BIGINT NOT NULL,
    order_id    BIGINT REFERENCES orders(id),
    used_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE(voucher_id, order_id)  -- prevent double-use
);

Apply voucher với race condition protection:

func ApplyVoucher(ctx context.Context, db *sql.DB, userID int64, code string, orderValue int64) (int64, error) {
    tx, _ := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
    defer tx.Rollback()
    
    // Lock voucher row
    var v Voucher
    err := tx.QueryRowContext(ctx, `
        SELECT id, type, value, min_order_value, max_discount, 
               max_usage_per_user, total_usage_limit
        FROM vouchers
        WHERE code = $1
          AND is_active = true
          AND now() BETWEEN valid_from AND valid_until
        FOR UPDATE
    `, code).Scan(&v.ID, &v.Type, &v.Value, &v.MinOrderValue, 
                   &v.MaxDiscount, &v.MaxUsagePerUser, &v.TotalUsageLimit)
    if err != nil {
        return 0, ErrVoucherNotFound
    }
    
    // Check min order value
    if orderValue < v.MinOrderValue {
        return 0, ErrMinOrderValueNotMet
    }
    
    // Check per-user usage
    var userUsageCount int
    tx.QueryRowContext(ctx, `
        SELECT COUNT(*) FROM voucher_usages
        WHERE voucher_id = $1 AND user_id = $2
    `, v.ID, userID).Scan(&userUsageCount)
    
    if userUsageCount >= v.MaxUsagePerUser {
        return 0, ErrVoucherLimitReached
    }
    
    // Check total usage
    if v.TotalUsageLimit > 0 {
        var totalUsageCount int
        tx.QueryRowContext(ctx, `
            SELECT COUNT(*) FROM voucher_usages WHERE voucher_id = $1
        `, v.ID).Scan(&totalUsageCount)
        
        if totalUsageCount >= v.TotalUsageLimit {
            return 0, ErrVoucherSoldOut
        }
    }
    
    // Calculate discount
    var discount int64
    switch v.Type {
    case "PERCENTAGE":
        discount = orderValue * v.Value / 100
        if v.MaxDiscount > 0 && discount > v.MaxDiscount {
            discount = v.MaxDiscount
        }
    case "FIXED_AMOUNT":
        discount = v.Value
    }
    
    // Reserve voucher (insert pending usage)
    _, err = tx.ExecContext(ctx, `
        INSERT INTO voucher_usages (voucher_id, user_id, order_id)
        VALUES ($1, $2, NULL)  -- order_id update sau khi order confirm
    `, v.ID, userID)
    if err != nil {
        return 0, err
    }
    
    tx.Commit()
    return discount, nil
}

5. Recommendation Engine

5.1 Collaborative Filtering

User-based CF:
  "Người dùng giống bạn (similar purchase history) đã mua gì?"

Item-based CF:
  "Người mua sản phẩm này cũng mua gì?"  ← Phổ biến hơn, stable hơn

Offline batch job (Spark):

from pyspark.ml.recommendation import ALS
from pyspark.sql import SparkSession

spark = SparkSession.builder.appName("ItemRecommendation").getOrCreate()

# Load order history
orders = spark.read.parquet("s3://data/orders_history")

# Prepare training data: (user_id, sku, implicit_rating)
interactions = orders.groupBy("user_id", "sku").agg(
    count("*").alias("purchase_count")
).withColumn(
    "rating", col("purchase_count").cast("float")  # implicit feedback
)

# ALS model
als = ALS(
    maxIter=10,
    regParam=0.1,
    userCol="user_id",
    itemCol="sku",
    ratingCol="rating",
    coldStartStrategy="drop",
    implicitPrefs=True  # implicit feedback (clicks/purchases)
)

model = als.fit(interactions)

# Generate item-to-item similarity
item_factors = model.itemFactors  # latent factors cho mỗi SKU
# Tính cosine similarity giữa các SKUs
# Lưu vào Redis: ZADD reco:IPH15P-256  0.95 SAMSG-S24  0.89 XIAOMI14

# Serving: Khi user xem iPhone 15 Pro → query Redis để lấy top similar items

5.2 Real-time Personalization

Feature Engineering:
  User features:
    - Demographics: age, gender, location
    - Behavioral: avg_order_value, purchase_frequency, favorite_categories
    - Session: current_cart_items, browsing_history_last_30mins
  
  Item features:
    - Category, brand, price_range
    - Popularity: views_7d, orders_7d, rating_avg
    - Merchant: merchant_rating, fulfillment_speed

Model: LightGBM Ranker
  Input:  user features + item features + context (time, device, weather?!)
  Output: CTR prediction score
  
  Train trên historical click/purchase data
  Deploy qua model serving (TensorFlow Serving / Triton)

Online serving:

import redis
import requests

def get_personalized_recommendations(user_id: int, context: dict) -> list:
    # Bước 1: Candidate Generation (fast retrieval)
    # Lấy 200 candidates từ multiple sources:
    candidates = []
    
    # Source 1: User's favorite categories
    fav_categories = redis.smembers(f"user:{user_id}:fav_categories")
    for cat in fav_categories[:3]:
        candidates.extend(redis.zrevrange(f"popular:{cat}", 0, 30))
    
    # Source 2: Collaborative filtering
    candidates.extend(redis.zrevrange(f"cf:{user_id}", 0, 50))
    
    # Source 3: Trending items
    candidates.extend(redis.zrevrange("trending:today", 0, 50))
    
    # Deduplicate
    candidates = list(set(candidates))[:200]
    
    # Bước 2: Ranking (ML model scoring)
    user_features = get_user_features(user_id)
    item_features_batch = get_item_features(candidates)
    
    scores = []
    for sku, item_feat in zip(candidates, item_features_batch):
        features = {**user_features, **item_feat, **context}
        score = model_server.predict(features)  # gRPC call to Triton
        scores.append((sku, score))
    
    # Sort by score descending
    scores.sort(key=lambda x: x[1], reverse=True)
    
    # Bước 3: Business rules & diversity
    # - Filter out-of-stock items
    # - Đảm bảo diverse categories (không recommend toàn iPhone)
    # - Boost margin (nếu có 2 items cùng score, chọn item có margin cao hơn)
    
    final_recommendations = apply_business_rules(scores[:50])
    return final_recommendations[:20]

6. Flash Sale Architecture

6.1 System Design

┌──────────────┐         ┌───────────────────┐         ┌──────────────┐
│              │         │  CDN (Cloudflare) │         │              │
│  User (app)  ├────────►│  - Static assets  ├────────►│  API Gateway │
│              │         │  - Rate limiting  │         │  (Kong/Envoy)│
└──────────────┘         └───────────────────┘         └───────┬──────┘
                                                               │
          ┌────────────────────────────────────────────────────┼────────┐
          │                                                    │        │
          ▼                                                    ▼        ▼
┌──────────────────┐                             ┌──────────────────────────┐
│ Flash Sale Svc   │                             │ Order Service            │
│ (Pre-reserve)    │                             │ (Async processing)       │
│                  │                             │                          │
│ Redis Cluster ───┼────sync every 10s──────────►│ PostgreSQL               │
│ (hot inventory)  │                             │ (source of truth)        │
└──────────────────┘                             └──────────────────────────┘
          │
          └───────► Message Queue (Kafka) ─────────► Workers (create order)

6.2 Rate Limiting — Token Bucket

-- Redis Lua script: Token bucket rate limiter
-- KEYS[1] = "rate_limit:user:{user_id}"
-- ARGV[1] = max_tokens (e.g., 10)
-- ARGV[2] = refill_rate (e.g., 1 token per second)
-- ARGV[3] = current_timestamp

local key = KEYS[1]
local max_tokens = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or max_tokens
local last_refill = tonumber(bucket[2]) or now

-- Refill tokens dựa trên thời gian trôi qua
local elapsed = now - last_refill
local refill = math.floor(elapsed * refill_rate)
tokens = math.min(max_tokens, tokens + refill)

if tokens >= 1 then
    tokens = tokens - 1
    redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
    redis.call('EXPIRE', key, 60)  -- TTL 60s
    return 1  -- allowed
else
    return 0  -- rate limited
end
func CheckRateLimit(ctx context.Context, rdb *redis.Client, userID int64) (bool, error) {
    script := `...` // Lua script ở trên
    
    result, err := rdb.Eval(ctx, script,
        []string{fmt.Sprintf("rate_limit:user:%d", userID)},
        10,                        // max 10 requests
        0.5,                       // refill 1 token per 2 seconds
        time.Now().Unix(),
    ).Int()
    
    return result == 1, err
}

6.3 Queue-based Order Processing

Thay vì xử lý order synchronously trong request:

User click "Mua"
  ↓
API: Reserve stock (Redis) + Publish Kafka event
  ↓ (return immediately với order_id pending)
User nhận response: "Đơn hàng đang xử lý, vui lòng chờ..."
  ↓
Background worker consume Kafka:
  - Validate payment
  - Sync stock to DB
  - Create order record
  - Send notification
  ↓
WebSocket push update: "Đơn hàng thành công!"
// Producer: API handler
func HandleFlashSalePurchase(w http.ResponseWriter, r *http.Request) {
    var req PurchaseRequest
    json.NewDecoder(r.Body).Decode(&req)
    
    // Rate limit
    allowed, _ := CheckRateLimit(ctx, redisClient, req.UserID)
    if !allowed {
        http.Error(w, "Too many requests", http.StatusTooManyRequests)
        return
    }
    
    // Reserve stock in Redis
    err := ReserveFlashSaleStock(ctx, redisClient, req.SKU, req.Quantity)
    if err != nil {
        http.Error(w, "Sold out", http.StatusConflict)
        return
    }
    
    // Publish to Kafka (async processing)
    orderID := uuid.New().String()
    event := OrderCreatedEvent{
        OrderID:  orderID,
        UserID:   req.UserID,
        SKU:      req.SKU,
        Quantity: req.Quantity,
    }
    kafkaProducer.Publish("order-created", orderID, event)
    
    // Return immediately
    json.NewEncoder(w).Encode(map[string]string{
        "order_id": orderID,
        "status":   "PENDING",
    })
}

// Consumer: Background worker
func ProcessOrderCreated(msg kafka.Message) error {
    var event OrderCreatedEvent
    json.Unmarshal(msg.Value, &event)
    
    // Actual DB transaction
    tx, _ := db.Begin()
    defer tx.Rollback()
    
    // Deduct stock from PostgreSQL
    _, err := tx.Exec(`
        UPDATE product_variants
        SET stock = stock - $1
        WHERE sku = $2 AND stock >= $1
    `, event.Quantity, event.SKU)
    if err != nil {
        // Rollback Redis reservation
        redisClient.HIncrBy(ctx, "flash:"+event.SKU, "stock", int64(event.Quantity))
        return err
    }
    
    // Create order
    tx.Exec(`INSERT INTO orders (...) VALUES (...)`)
    tx.Commit()
    
    // Push notification via WebSocket
    websocketHub.SendToUser(event.UserID, "order_confirmed", event.OrderID)
    return nil
}

7. Interview Questions

7.1 System Design

Q: Thiết kế hệ thống flash sale cho 1 triệu concurrent users trong 5 phút?

Sketch:

  • CDN cho static assets, rate limiting
  • API Gateway với token bucket per user
  • Redis cluster cho hot inventory (master-replica, cluster mode)
  • Kafka queue + worker pool xử lý async
  • PostgreSQL primary-replica cho persistent data
  • WebSocket để push notification real-time

Trade-offs discuss:

  • Eventual consistency (Redis vs DB) chấp nhận được
  • Queue có thể bị backlog → user experience: "Đơn hàng đang xử lý"
  • Redis failure → fallback to DB (slower) hoặc reject requests

Q: Làm thế nào để prevent overselling khi có 1000 servers cùng trừ stock?

Answer:

  1. Pessimistic lock (FOR UPDATE) → chậm, không scale
  2. Optimistic lock (version field) → user retry nhiều
  3. Redis atomic DECR → best cho flash sale, eventual sync to DB
  4. Distributed lock (Redlock, etcd lease) → complex, overkill

Recommend: Redis DECR với Lua script để atomic check + reserve.


7.2 Trade-off Questions

Q: Khi nào dùng Elasticsearch, khi nào dùng PostgreSQL full-text search?

Answer:

  • PostgreSQL FTS: <10M records, simple search, muốn ACID guarantee
  • Elasticsearch: >10M records, complex ranking, faceted search, analytics

Trade-off:

  • ES tốn thêm infra, operational complexity
  • ES eventual consistency (sync lag từ DB)
  • PostgreSQL FTS chậm hơn nhưng strongly consistent

Q: Tại sao snapshot giá vào order_items thay vì join với product_variants?

Answer:

  • Giá có thể thay đổi sau khi user mua
  • User phải trả đúng giá tại thời điểm checkout
  • Historical reporting: revenue report phải chính xác theo giá past
  • Compliance: hóa đơn điện tử không được sửa đổi

Tóm tắt

E-commerce không phải chỉ CRUD. Những vấn đề thực tế:

  • Consistency: Overselling có thể phá sản doanh nghiệp
  • Performance: Flash sale 100k RPS trong 5 phút
  • Fraud: Voucher abuse, fake reviews, bot mua hàng
  • Business logic: Dynamic pricing, recommendation, promotion rules

Các pattern quan trọng:

  • Optimistic locking với version field
  • Redis pre-cache cho hot data
  • Queue-based async processing
  • Saga pattern cho distributed transaction
  • ML ranking cho personalization

Tài liệu tham khảo