Skip to content

🏗️ 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

ModuleNội dung
AuthJWT access + refresh token (lưu DB, không cần Redis)
RBAC2 role: Owner (bạn) và Customer — boolean flag
CampaignCRUD, tier pricing, capacity management
OrderĐặt slot giữ 15 phút, atomic SQL chống oversell (UPDATE ... WHERE taken + qty <= capacity RETURNING)
Deposit ExpiryIn-process goroutine scan mỗi 30s, huỷ đơn quá hạn — chưa cần queue
PaymentSePay webhook: verify Apikey → idempotent (unique key) → mark DEPOSIT_HELD
SettlementKhi campaign CLOSED → khoá tier → tính amount_due → admin xác nhận thủ công
NotificationChư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 đổiLý do
Deposit Expiry: goroutine → Asynq delayed jobReliable, không mất khi restart, có retry
Thêm Telegram BotNhận thông báo: đơn mới, đơn hết hạn, webhook đến
Thêm RedisChạy Asynq queue — sau này dùng luôn cho cache
Webhook xử lý asyncVerify 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

ModuleNội dung
SSE RealtimeSlot counter cập nhật trực tiếp cho mọi client. Khi có đơn → Redis Pub/Sub → SSE Hub → broadcast
Rate LimitingPer-IP: 10 req/s cho API, 1 req/5s cho đặt slot. Lưu counter trong Redis (sliding window)
Redis CacheCache 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

ModuleNội dung
Tier LockingAtomic: SELECT ... FOR UPDATE → SUM qty → tra bảng tier → UPDATE campaigns SET locked_tier_id
SettlementBatchWorker sinh bill hàng loạt (chunk 100), resumable, ON CONFLICT DO NOTHING
Deadline ScannerScan mỗi phút, FOR UPDATE SKIP LOCKED — nhiều instance song song OK
RefundEngineHoàn cọc resumable, idempotent key (refund:{order_id}), crash → retry từ cursor
ReconciliationCron hàng ngày đối soát payments vs orders, lệch → alert
Outbox PatternMọ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 delivered

Khi 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 đổiLý 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 + ProtobufNhanh 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
PgBouncerConnection pooling, chịu nhiều concurrent connection hơn
CasbinPermission-based RBAC: order:refund, campaign:create — chi tiết từng action
OpenTelemetryTrace 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 ──▶ SHIPPED

Họ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

ModuleNội dung
Multi-tenantRow-Level Security (RLS): mỗi query tự lọc theo workspace_id
Workspace RBACUser → nhiều Workspace → mỗi Workspace có Role riêng → Permissions riêng
Invite systemInvite link cho nhân viên vào workspace
Crawler PipelinePlaywright → LLM extract → Dedup → Draft PENDING_REVIEW → Approve → Publish
Price Change DetectionRe-crawl campaign đang OPEN, lệch > 5% → alert
BillingSubscription cho SaaS (Stripe)
Media CDNLuôn download media về MinIO/R2, không hotlink

📊 BẢNG TỔNG QUAN

Phase 0Phase 1Phase 2Phase 3Phase 4Phase 5
TênFirst OrderOperationalRealtimeSettlementScalePlatform
Nỗi đauChưa bán đượcNgồi canh thủ côngKhông có realtimeĐóng nhóm thủ côngServer nghẽnShop khác muốn dùng
Binary111133+
Queue❌ (in-proc cron)AsynqAsynqAsynq + OutboxAsynq + Outbox+ Debezium
Redis✅ (queue)✅ (+ cache + SSE)
RealtimeSSESSESSESSE
RBACis_adminis_adminis_adminis_adminCasbinMulti-workspace
PaymentSePay + thủ công+ Telegram notify+ Telegram notify+ Auto settlement+ Auto settlement+ Stripe billing
Anti-oversellAtomic SQLAtomic SQLAtomic SQLAtomic SQLgRPC EnginegRPC Engine
Observabilityslogslog JSONslog JSONslog JSONOTel fullOTel 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