Appearance
🏗️ LỘ TRÌNH KIẾN TRÚC: MVP → SAAS (GO-FIRST)
Bối cảnh: Solo-dev học Go · Áp dụng cho shop riêng trước → Scale đa role → Multi-tenant SaaS Nguyên tắc: Mỗi phase phải dùng được ngay, không cần chờ phase sau mới có tính năng quan trọng.
🟢 PHASE 0: FIRST ORDER — "Bán được đơn đầu tiên"
Mục tiêu: Hoàn thành luồng đặt cọc từ A→Z, chạy production cho shop mình.
👤 SHOPPER 👤 SHOP OWNER (Bạn)
│ │
│ HTTPS │ HTTPS
▼ ▼
┌──────────────────────────────────────┐
│ FRONTEND · Next.js (App Router) │
│ ├─ Storefront (đặt cọc, xem slot) │
│ └─ Admin (tạo campaign, xem đơn) │
└──────────────┬───────────────────────┘
│ REST JSON + JWT
▼
┌──────────────────────────────────────┐
│ GO MONOLITH (1 binary duy nhất) │
│ ├─ Auth (JWT + refresh token) │
│ ├─ RBAC (is_admin bool) │
│ ├─ Campaign CRUD │
│ ├─ Order (giữ slot 15') │
│ ├─ Deposit Expiry (in-process cron) │
│ ├─ Payment (SePay webhook) │
│ └─ Settlement cơ bản (thu còn lại) │
└──────────────┬───────────────────────┘
│ pgx + sqlc
▼
┌──────────────────────────────────────┐
│ PostgreSQL (Source of Truth) │
└──────────────────────────────────────┘Tính năng
| Module | Nội dung |
|---|---|
| Auth | JWT access + refresh token (lưu DB, không cần Redis) |
| RBAC | 2 role: Owner (bạn) và Customer — boolean flag |
| Campaign | CRUD, tier pricing, capacity management |
| Order | Đặt slot giữ 15 phút, atomic SQL chống oversell (UPDATE ... WHERE taken + qty <= capacity RETURNING) |
| Deposit Expiry | In-process goroutine scan mỗi 30s, huỷ đơn quá hạn — chưa cần queue |
| Payment | SePay webhook: verify Apikey → idempotent (unique key) → mark DEPOSIT_HELD |
| Settlement | Khi campaign CLOSED → khoá tier → tính amount_due → admin xác nhận thủ công |
| Notification | Chưa có — admin xem trực tiếp trên dashboard |
Khi nào lên phase 1?
Khi bạn mệt vì phải tự kiểm tra thủ công — muốn hệ thống tự huỷ đơn, tự gửi thông báo, tự xử lý webhook không chặn API.
🟡 PHASE 1: OPERATIONAL — "Tự động hoá vận hành"
Mục tiêu: Hệ thống tự chạy, không cần bạn ngồi canh.
👤 SHOPPER 👤 SHOP OWNER
│ │
▼ ▼
┌──────────────────────────────────────┐
│ FRONTEND · Next.js │
│ └─ + Telegram Bot (admin command) │
└──────────────┬───────────────────────┘
│ REST + JWT
▼
┌──────────────────────────────────────┐
│ GO MONOLITH │
│ ├─ ... (giữ nguyên Phase 0) │
│ ├─ Asynq Scheduler (thay in-proc) │
│ └─ Notifier (Telegram Bot) │
└──────────────┬───────────────────────┘
│
┌───────┴───────┐
▼ ▼
┌─────────────┐ ┌──────────────────┐
│ PostgreSQL │ │ Redis (Asynq) │
└─────────────┘ └──────────────────┘Thay đổi so với Phase 0
| Thay đổi | Lý do |
|---|---|
| Deposit Expiry: goroutine → Asynq delayed job | Reliable, không mất khi restart, có retry |
| Thêm Telegram Bot | Nhận thông báo: đơn mới, đơn hết hạn, webhook đến |
| Thêm Redis | Chạy Asynq queue — sau này dùng luôn cho cache |
| Webhook xử lý async | Verify HMAC sync → enqueue → worker xử lý → API trả 200 ngay |
| Structured logging (slog JSON) | Debug production dễ hơn |
Ghi nhớ về Queue system
Asynq được chọn cho Phases 1–4 vì đây là Go-native job queue — tự nhiên với Go monolith, không cần thêm dependency Node.js. Khi hệ thống tách ra microservices có Node.js components, có thể cân nhắc chuyển sang BullMQ (nếu dùng NestJS gateway) hoặc giữ Asynq (nếu worker toàn Go). Không bắt buộc phải nhất quán ngay từ đầu — mỗi phase chọn tool phù hợp nhất với runtime của nó.
Không làm ở phase này
❌ Chưa tách binary (monolith vẫn ổn) ❌ Chưa SSE realtime ❌ Chưa RBAC phức tạp ❌ Chưa rate limiting
Khi nào lên phase 2?
Khi campaign có 50+ người truy cập cùng lúc — cần realtime đếm slot để tạo urgency, cần chống spam.
🟠 PHASE 2: REALTIME & GROWTH — "Mở bán live, chống spam"
Mục tiêu: Tạo trải nghiệm mua hàng live (đếm slot realtime), bảo vệ API khỏi spam/abuse.
👤 SHOPPER (50-500 concurrent)
│
▼
┌──────────────────────────────────────┐
│ FRONTEND · Next.js + SSE │
│ └─ Slot counter realtime │
└──────────────┬───────────────────────┘
│ REST + SSE + JWT
▼
┌──────────────────────────────────────┐
│ GO API SERVER (binary #1) │
│ ├─ SSE Hub (goroutine + channel) │
│ ├─ Rate Limiter (per-IP, Redis) │
│ ├─ Redis Pub/Sub → SSE fan-out │
│ └─ Business Modules │
└──────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ PostgreSQL │ Redis (cache + queue) │
└──────────────────────────────────────┘Tính năng mới
| Module | Nội dung |
|---|---|
| SSE Realtime | Slot counter cập nhật trực tiếp cho mọi client. Khi có đơn → Redis Pub/Sub → SSE Hub → broadcast |
| Rate Limiting | Per-IP: 10 req/s cho API, 1 req/5s cho đặt slot. Lưu counter trong Redis (sliding window) |
| Redis Cache | Cache campaign detail (TTL 30s), giảm tải DB khi nhiều người xem cùng campaign |
| Health check | /health endpoint: check DB + Redis connection |
Học Go
- Goroutines + Channels (SSE Hub)
- Redis Pub/Sub
- Sliding window rate limiter
Khi nào lên phase 3?
Khi campaign đóng nhóm — cần tính giá chốt, sinh bill hàng loạt, xử lý hoàn cọc tự động.
🔵 PHASE 3: SETTLEMENT & TRUST — "Đóng nhóm, thu tiền, hoàn cọc"
Mục tiêu: Tự động hoá toàn bộ luồng tiền sau khi campaign đóng — không can thiệp thủ công.
Campaign CLOSED
│
▼
┌──────────────────────────────────────────────┐
│ SETTLEMENT PIPELINE │
│ │
│ ① Khoá tier (atomic) │
│ SUM(qty WHERE DEPOSIT_HELD) → tra tier │
│ │
│ ② SettlementBatch (worker, chunk 100) │
│ Sinh bill: amount_due = qty × final │
│ + shipping − deposit │
│ Gửi Telegram + QR cho từng user │
│ │
│ ③ Deadline Scanner (worker, scan mỗi phút) │
│ Quá hạn + grace 10' → CANCELLED │
│ → enqueue RefundEngine │
│ │
│ ④ RefundEngine (resumable, chunk 100) │
│ Idempotent key chống double-refund │
│ Gọi provider → mark REFUNDED │
│ │
│ ⑤ Reconciliation Cron (hàng ngày) │
│ SUM(payments) vs SUM(orders.deposit) │
│ Lệch → Telegram alert │
└──────────────────────────────────────────────┘Tính năng mới
| Module | Nội dung |
|---|---|
| Tier Locking | Atomic: SELECT ... FOR UPDATE → SUM qty → tra bảng tier → UPDATE campaigns SET locked_tier_id |
| SettlementBatch | Worker sinh bill hàng loạt (chunk 100), resumable, ON CONFLICT DO NOTHING |
| Deadline Scanner | Scan mỗi phút, FOR UPDATE SKIP LOCKED — nhiều instance song song OK |
| RefundEngine | Hoàn cọc resumable, idempotent key (refund:{order_id}), crash → retry từ cursor |
| Reconciliation | Cron hàng ngày đối soát payments vs orders, lệch → alert |
| Outbox Pattern | Mọi state transition ghi event trong cùng transaction → worker xử lý side effects |
Outbox Pattern (bắt đầu từ phase này)
sql
-- Mọi state change đều ghi event trong cùng 1 transaction
BEGIN;
UPDATE orders SET status = 'SETTLEMENT_DUE' WHERE ...;
INSERT INTO outbox (event_type, payload, created_at)
VALUES ('settlement.due', '{"order_id": ...}', now());
COMMIT;
-- Outbox relay (worker) poll mỗi 1s → dispatch → mark deliveredKhi nào lên phase 4?
Khi campaign có 1000+ người truy cập cùng lúc — API nghẽn ở hot path (đặt slot), cần tách riêng.
🟣 PHASE 4: SCALE — "Chống sập khi flash sale"
Mục tiêu: Tách hot path (đặt slot) thành service riêng, chịu tải cao, quan sát được hệ thống.
👤 SHOPPER (1000+ concurrent)
│
▼
┌──────────────────────────────────────────────────┐
│ FRONTEND · Next.js + SSE │
└──────────────────┬───────────────────────────────┘
│ REST
▼
┌──────────────────────────────────────────────────┐
│ GO API GATEWAY (binary #1) │
│ ├─ RBAC Permission-based (Casbin) │
│ ├─ Rate Limiting (nâng cấp per-user + per-IP) │
│ └─ Điều phối request │
└──────┬─────────────────────────┬─────────────────┘
│ gRPC (hot path) │ enqueue (async)
▼ ▼
┌────────────────────┐ ┌──────────────────────┐
│ GO ORDER ENGINE │ │ Queue (Asynq/Redis) │
│ (binary #2) │ └──────────┬───────────┘
│ ├─ Atomic SQL │ │
│ ├─ Chống oversell │ ▼
│ └─ Protobuf/gRPC │ ┌──────────────────────┐
└────────┬───────────┘ │ GO WORKERS (binary #3)│
│ │ ├─ SettlementBatch │
│ │ ├─ RefundEngine │
│ │ ├─ Notifier │
│ │ └─ Outbox Relay │
▼ └──────────┬─────────────┘
┌──────────────────────────────────────────────────┐
│ DATA LAYER │
│ PostgreSQL + PgBouncer │ Redis │
└──────────────────────────────────────────────────┘
═══ TẤT CẢ PHÁT TELEMETRY ═══
▼
┌──────────────────────┐
│ OTel Collector │
│ → Jaeger/Prom/Grafana│
└──────────────────────┘Thay đổi kiến trúc
| Thay đổi | Lý do |
|---|---|
| Tách Go Order Engine (binary riêng) | Hot path cần tối ưu riêng, không bị ảnh hưởng bởi business logic khác |
| Giao tiếp gRPC + Protobuf | Nhanh hơn REST, type-safe contract, streaming cho SSE backend |
| Tách Worker (binary #3) | Scale độc lập, không ảnh hưởng API server |
| PgBouncer | Connection pooling, chịu nhiều concurrent connection hơn |
| Casbin | Permission-based RBAC: order:refund, campaign:create — chi tiết từng action |
| OpenTelemetry | Trace xuyên suốt: browser → gateway → engine → DB → worker → notification |
Order State Machine (hoàn chỉnh)
┌──────────────┐
đặt ───▶ │ RESERVED │──── hết hạn ───▶ EXPIRED
└──────┬───────┘
webhook cọc (idempotent)
▼
┌──────────────┐
│ DEPOSIT_HELD │◀── đếm vào MOQ/tier
└──────┬───────┘
campaign CLOSED → khoá tier
▼
┌──────────────┐
│ SETTLEMENT │◀── SettlementBatch sinh bill
└──┬───────┬───┘
trả đúng│ │quá hạn
▼ ▼
┌──────────┐ ┌──────────────┐
│CONFIRMED │ │ CANCELLED │──▶ REFUNDING ──▶ REFUNDED
└────┬─────┘ └──────────────┘ (RefundEngine)
▼
FULFILLING ──▶ SHIPPEDHọc Go
- gRPC/Protobuf · Interceptors
- Distributed tracing (OTel SDK)
- Connection pooling (PgBouncer)
Khi nào lên phase 5?
Khi có shop khác muốn dùng hệ thống.
🔴 PHASE 5: PLATFORM — "Mở SaaS cho mọi creator"
Mục tiêu: Nhiều shop cùng chạy, dữ liệu cách ly, tự động crawl nguồn hàng.
👤 SHOPPER A 👤 SHOPPER B 👤 CREATOR A 👤 CREATOR B
│ │ │ │
▼ ▼ ▼ ▼
┌────────────────────────────────────────────────────┐
│ FRONTEND · Next.js (Workspace-aware) │
└───────────────────────┬────────────────────────────┘
│ REST/gRPC + JWT
▼
┌────────────────────────────────────────────────────┐
│ GO API GATEWAY │
│ ├─ Tenant Resolution (shop nào?) │
│ ├─ RBAC đa tầng: User → Workspace → Role → Perm │
│ └─ Rate limit per tenant │
└────┬──────────────┬────────────────────┬───────────┘
│ gRPC │ enqueue │ gRPC
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌────────────────────┐
│ ORDER │ │ Queue │ │ CRAWLER SERVICE │
│ ENGINE │ └────┬─────┘ │ (Playwright + LLM)│
└────┬─────┘ ▼ │ ├─ Auto-crawl │
│ ┌──────────┐ │ ├─ LLM Extract │
│ │ WORKERS │ │ └─ Draft Review │
│ └────┬─────┘ └─────────┬──────────┘
▼ ▼ ▼
┌────────────────────────────────────────────────────┐
│ DATA LAYER (Multi-tenant) │
│ PostgreSQL (Row-Level Security per workspace_id) │
│ Redis │ MinIO/R2 (media) │
└────────────────────────────────────────────────────┘Tính năng mới
| Module | Nội dung |
|---|---|
| Multi-tenant | Row-Level Security (RLS): mỗi query tự lọc theo workspace_id |
| Workspace RBAC | User → nhiều Workspace → mỗi Workspace có Role riêng → Permissions riêng |
| Invite system | Invite link cho nhân viên vào workspace |
| Crawler Pipeline | Playwright → LLM extract → Dedup → Draft PENDING_REVIEW → Approve → Publish |
| Price Change Detection | Re-crawl campaign đang OPEN, lệch > 5% → alert |
| Billing | Subscription cho SaaS (Stripe) |
| Media CDN | Luôn download media về MinIO/R2, không hotlink |
📊 BẢNG TỔNG QUAN
| Phase 0 | Phase 1 | Phase 2 | Phase 3 | Phase 4 | Phase 5 | |
|---|---|---|---|---|---|---|
| Tên | First Order | Operational | Realtime | Settlement | Scale | Platform |
| Nỗi đau | Chưa bán được | Ngồi canh thủ công | Không có realtime | Đóng nhóm thủ công | Server nghẽn | Shop khác muốn dùng |
| Binary | 1 | 1 | 1 | 1 | 3 | 3+ |
| Queue | ❌ (in-proc cron) | Asynq | Asynq | Asynq + Outbox | Asynq + Outbox | + Debezium |
| Redis | ❌ | ✅ (queue) | ✅ (+ cache + SSE) | ✅ | ✅ | ✅ |
| Realtime | ❌ | ❌ | SSE | SSE | SSE | SSE |
| RBAC | is_admin | is_admin | is_admin | is_admin | Casbin | Multi-workspace |
| Payment | SePay + thủ công | + Telegram notify | + Telegram notify | + Auto settlement | + Auto settlement | + Stripe billing |
| Anti-oversell | Atomic SQL | Atomic SQL | Atomic SQL | Atomic SQL | gRPC Engine | gRPC Engine |
| Observability | slog | slog JSON | slog JSON | slog JSON | OTel full | OTel full |
| Crawler | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
🎯 QUY TẮC CHUYỂN PHASE
Phase 0 ──► Phase 1: Khi bạn MỆT vì phải kiểm tra thủ công
Phase 1 ──► Phase 2: Khi campaign có 50+ người TRUY CẬP CÙNG LÚC
Phase 2 ──► Phase 3: Khi campaign ĐÓNG NHÓM, cần thu tiền tự động
Phase 3 ──► Phase 4: Khi server NGHẼN lúc mở bán (1000+ concurrent)
Phase 4 ──► Phase 5: Khi có SHOP KHÁC muốn dùng hệ thống⚠️ Nguyên tắc vàng:
- Không nhảy phase khi chưa có "nỗi đau" thực tế
- Mỗi phase phải dùng được ngay — không có chuyện "phase sau mới có tính năng quan trọng"
- Phase 0 đã bao gồm: deposit expiry, webhook idempotency, settlement cơ bản — đủ để bán hàng an toàn