Subscription (đăng ký theo gói) là mô hình cốt lõi của rất nhiều sản phẩm hiện đại như SaaS, e-learning, API service, AI tool. Tuy nhiên, rất nhiều hệ thống Subscription được xây dựng sai ngay từ đầu, dẫn đến khó mở rộng, lỗi phân quyền, sai trạng thái gia hạn, và khó tích hợp thanh toán.
Bài viết này sẽ đi từng lớp một, từ tư duy nghiệp vụ, database, API, billing, gia hạn, hủy gói, đến best practices thực tế trong production.

1. Subscription là gì và vì sao không nên làm đơn giản
Nhiều hệ thống mới chỉ làm Subscription theo kiểu:
-
User có cột plan = pro
-
Hoặc expired_at
Cách này sai về mặt kiến trúc, vì Subscription thực tế có:
-
Nhiều gói
-
Nhiều chu kỳ
-
Trạng thái phức tạp
-
Lịch sử thanh toán
-
Gia hạn tự động
-
Nâng cấp / hạ cấp
-
Tạm ngưng / khôi phục
Subscription là một domain nghiệp vụ riêng, không phải chỉ là 1 cột trong bảng user.
2. Các khái niệm cốt lõi cần hiểu rõ
Trước khi viết code, cần phân biệt rõ:
2.1 Plan (Gói dịch vụ)
-
Free
-
Pro
-
Business
-
Enterprise
Plan chỉ là định nghĩa, không gắn trực tiếp với user.
2.2 Subscription
-
Là mối quan hệ giữa User ↔ Plan
-
Có thời gian hiệu lực
-
Có trạng thái
2.3 Billing Cycle
-
Monthly
-
Quarterly
-
Yearly
2.4 Subscription Status
Không chỉ có active / inactive:
-
trialing
-
active
-
past_due
-
canceled
-
expired
-
paused
3. Thiết kế Database chuẩn cho Subscription
3.1 Bảng plans
plans
- id
- code (free, pro, business)
- name
- price
- currency
- interval (month, year)
- interval_count
- is_active
Plan không gắn user. Plan chỉ là template.
3.2 Bảng subscriptions
subscriptions
- id
- user_id
- plan_id
- status
- started_at
- current_period_start
- current_period_end
- canceled_at
- created_at
- updated_at
Nguyên tắc:
-
Không ghi đè subscription cũ
-
Mỗi lần mua / gia hạn tạo record mới hoặc update theo chiến lược rõ ràng
3.3 Bảng subscription_payments
subscription_payments
- id
- subscription_id
- amount
- currency
- payment_method
- transaction_id
- status
- paid_at
Tách payment ra khỏi subscription giúp:
-
Dễ audit
-
Dễ hoàn tiền
-
Dễ tích hợp nhiều cổng thanh toán
4. Luồng tạo Subscription cơ bản
4.1 User chọn gói
Frontend:
-
Gọi API lấy danh sách plan
-
Hiển thị giá, chu kỳ
4.2 Backend xử lý
POST /subscriptions
1. Validate user
2. Validate plan
3. Tạo subscription với status = pending
4. Redirect sang cổng thanh toán
5. Xử lý thanh toán (Stripe / PayPal / ZaloPay)
5.1 Không tin frontend
Thanh toán luôn phải xác nhận bằng webhook.
Luồng chuẩn:
-
User thanh toán
-
Gateway gọi webhook
-
Backend verify chữ ký
-
Update subscription → active
5.2 Ví dụ logic webhook
if (event.type === 'invoice.paid') {
updateSubscription({
status: 'active',
current_period_end: nextPeriod
})
}
6. Gia hạn tự động (Auto Renewal)
Không dùng cron đơn giản kiểu:
if (expired_at < now) disable()
Thay vào đó:
-
Dựa vào current_period_end
-
Dựa vào trạng thái payment
Chiến lược tốt:
-
Gia hạn thành công → extend period
-
Gia hạn thất bại → past_due
-
Retry theo policy
7. Nâng cấp / hạ cấp gói
7.1 Upgrade ngay lập tức
-
Tính tiền chênh lệch
-
Update subscription
-
Giữ nguyên chu kỳ
7.2 Downgrade cuối kỳ
-
Đánh dấu pending_plan
-
Chỉ apply khi sang chu kỳ mới
8. Hủy Subscription đúng cách
Không nên delete subscription.
Hủy đúng nghĩa:
-
Set canceled_at
-
Status = canceled
-
Vẫn cho dùng tới hết kỳ
9. Phân quyền theo Subscription
Không nên viết:
if (user.plan === 'pro')
Nên viết:
can(user).access('export_data')
Mapping:
-
Plan → Feature
-
Feature → Permission
10. Những lỗi phổ biến khi làm Subscription
-
Gắn plan trực tiếp vào user
-
Không có trạng thái trung gian
-
Không lưu lịch sử thanh toán
-
Tin kết quả trả về từ frontend
-
Không xử lý retry payment
-
Không tách billing khỏi business logic
11. Best Practices trong Production
-
Tách Subscription Service
-
Webhook idempotent
-
Log mọi event billing
-
Có dashboard admin xem trạng thái
-
Không hardcode plan trong code
12. Khi nào nên tự build, khi nào dùng dịch vụ ngoài
Tự build khi:
-
Logic đặc thù
-
Bán B2B
-
Cần kiểm soát dữ liệu
Dùng Stripe / Paddle khi:
-
Startup nhỏ
-
Time-to-market nhanh
-
Ít custom logic
Kết luận
Subscription không phải là tính năng nhỏ, mà là xương sống của sản phẩm. Thiết kế sai sẽ kéo theo rất nhiều vấn đề về sau, từ billing, phân quyền, đến trải nghiệm người dùng.
Nếu làm đúng ngay từ đầu:
-
Code sạch
-
Dễ mở rộng
-
Dễ scale
-
Ít bug







