VietBilling
All guides
VietBilling Engineering Guides

Designing Products, Prices, Subscriptions, and entitlements

A practical VND SaaS data model with versioned prices, historical snapshots, subscription state, and payment-independent entitlements.

VietBilling EngineeringVerified 2026-08-3112 min read

Separate catalog, commerce, and access

A maintainable billing model keeps three concerns apart:

Product 1─* ProductPrice
Product *─* Benefit
Customer 1─* Checkout 1─0..1 Order 1─* Payment
Customer 1─* Subscription 1─* BenefitGrant
Subscription *─1 Product

Preserve historical prices

When a price moves from 99,000 VND to 149,000 VND, archive the old ProductPrice and create a new one. Orders and Subscriptions keep the purchased version or a snapshot so current catalog edits never rewrite history.

type ProductPrice = {
  id: string
  productId: string
  amount: number
  currency: "VND"
  interval: "month" | "year"
  archivedAt: string | null
}

Use integer VND amounts rather than floating point. Keep currency and interval explicit even with one supported currency because the invariant matters.

Checkout is not Payment

Checkout orchestrates an attempt: idempotency, provider reference, redirects, and link-creation state. Payment records the monetary result that can arrive later. Their separation represents the important timeout where the provider created a link but your app missed the response.

Order stores the purchased product, price, and period snapshot. Transaction is an immutable money record; never rewrite an old transaction to represent a later state.

Subscription is a state machine

incomplete → active → past_due → canceled
                    ↘ active after renewal
active + cancel_at_period_end → canceled at period end

Route every transition through one billing engine that validates current state and uses a transaction. Controllers, webhook handlers, and cron functions should not each update status independently.

Benefits and entitlement

A Product can include Benefits such as feature access or a customer limit. An active Subscription materializes BenefitGrants with validity periods. The merchant app reads grants or synchronizes them through signed webhooks.

This keeps authorization independent from the latest Payment row and avoids adding one Subscription column for every feature.

Tenant boundaries and constraints

Every business entity belongs to an Organization. A Public API key selects that Organization; public URLs should not accept an Organization ID supplied by the client.

Useful unique keys include (organization_id, external_customer_id), (organization_id, idempotency_key, operation), provider payment reference, and (subscription_id, period_start) for renewals.

Before adding a constraint, inspect duplicates and plan the backfill. Test price history, retry behavior, tenant isolation, late payments, and grant revocation.