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:

  1. User thanh toán

  2. Gateway gọi webhook

  3. Backend verify chữ ký

  4. 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

  1. Gắn plan trực tiếp vào user

  2. Không có trạng thái trung gian

  3. Không lưu lịch sử thanh toán

  4. Tin kết quả trả về từ frontend

  5. Không xử lý retry payment

  6. 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