Low-Level Design: Subscription Billing — Recurring Charges, Proration, and Dunning Management

Core Entities

Plan: plan_id, name, billing_cycle (MONTHLY, ANNUAL, WEEKLY), price_cents, currency, trial_days, features (JSON). Subscription: subscription_id, customer_id, plan_id, status (TRIALING, ACTIVE, PAST_DUE, CANCELLED, PAUSED), current_period_start, current_period_end, trial_end, cancel_at_period_end, payment_method_id. Invoice: invoice_id, subscription_id, customer_id, status (DRAFT, OPEN, PAID, VOID, UNCOLLECTIBLE), amount_due_cents, amount_paid_cents, period_start, period_end, due_date, paid_at. InvoiceItem: item_id, invoice_id, description, amount_cents, quantity, proration (boolean). PaymentAttempt: attempt_id, invoice_id, amount_cents, status (PENDING, SUCCEEDED, FAILED), failure_code, attempted_at.

Subscription Lifecycle

Trial: subscription starts in TRIALING for trial_days. No charge during trial. When trial ends: transition to ACTIVE and charge the first period. Active: charge at the start of each period. If payment succeeds: extend current_period_end by one billing cycle. If payment fails: transition to PAST_DUE, begin the dunning process. Cancellation: two modes: (1) cancel_at_period_end=true: subscription remains ACTIVE and customer keeps access until current_period_end, then cancels. (2) Immediate cancellation: prorate a refund for unused time and cancel immediately. Pause: temporarily suspend billing (customer keeps access, no charges). Resume on a future date. Used for seasonal subscriptions.

Proration Logic

When a customer upgrades mid-cycle: charge for the new plan from now until period_end, credit for the old plan from now until period_end. Proration formula: credit = old_price * (days_remaining / days_in_period). Charge = new_price * (days_remaining / days_in_period). Net charge = charge – credit. Always round up to the nearest cent. Example: $10/month plan, 15 days into a 30-day month, upgrades to $20/month. Credit = $10 * (15/30) = $5. Charge = $20 * (15/30) = $10. Net = $5 due immediately. Create an InvoiceItem with proration=true for the credit and another for the charge. Add to the current draft invoice (if mid-cycle) or the next invoice (end of cycle).

Dunning Management

Dunning: the process of recovering failed subscription payments. When a charge fails: update invoice status to OPEN, subscription status to PAST_DUE. Dunning schedule: retry Day 1, Day 3, Day 7, Day 14. On each retry: attempt payment via the saved payment method. If card expired: send an email asking the customer to update their payment method with a magic link. If retry succeeds: mark invoice PAID, restore subscription to ACTIVE. If all retries fail: mark invoice UNCOLLECTIBLE, cancel the subscription, send final cancellation email. Smart retry: retry when more likely to succeed — try on the day of the month the customer is most often successfully charged (analyze historical payment success by day). Avoid retrying on weekends. Use Stripe’s smart retries or implement the schedule with a cron job.

Metered / Usage-Based Billing

Some plans charge based on usage (API calls, GB stored, seats). Metered billing: track usage events during the billing period. Record every event: (subscription_id, metric, quantity, timestamp). At period end: aggregate total usage, compute the charge. Price tiers: first 1000 API calls free, $0.001 per call for 1001-100,000, $0.0005 per call above 100,000. Aggregation: a batch job runs at the end of each billing period, sums usage by tier, creates InvoiceItems for each tier. Real-time usage tracking: ingest usage events via API or Kafka. Store in a time-series DB. Provide customers a real-time usage dashboard with projected costs for the current period (prevent bill shock). Usage alerts: send an email when a customer has used 80% of their included quota.

Revenue Recognition

For annual subscriptions paid upfront: record the full payment as deferred revenue, recognize monthly (1/12 per month). This is required by GAAP (ASC 606). Technically: when a $120 annual subscription is paid: debit Cash $120, credit Deferred Revenue $120. Each month: debit Deferred Revenue $10, credit Revenue $10. Track current_period_revenue and deferred_revenue per subscription. Monthly revenue report: sum of all revenue recognized in the current month. This differs from cash collected in the month (new annual subscriptions inflate cash but not revenue). Expose both metrics for financial reporting.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do you calculate proration for a mid-cycle plan upgrade?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Proration credits the unused portion of the current plan and charges for the new plan from today until the current period end. Formula: days_remaining = (current_period_end – today).days. days_in_period = (current_period_end – current_period_start).days. credit = old_plan_price * (days_remaining / days_in_period). charge = new_plan_price * (days_remaining / days_in_period). net_charge = charge – credit. Round both to the nearest cent before subtracting to avoid floating-point errors. Always use integer cents: credit_cents = old_price_cents * days_remaining // days_in_period (integer division). Create two InvoiceItems: a negative one (credit) and a positive one (charge). Net immediately (charge the difference now) or add to the next invoice (simpler but the customer gets a windfall on the upgrade date). Stripe prorates and creates an invoice immediately; the customer is charged the net difference.”
}
},
{
“@type”: “Question”,
“name”: “How does the dunning process reduce involuntary churn from failed payments?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Involuntary churn: subscriptions cancelled not because the customer chose to leave, but because their payment failed (expired card, insufficient funds, bank fraud hold). Dunning reduces this by: (1) Retry schedule: attempt payment on day 1, 3, 7, 14 after failure. Exponential spacing avoids repeatedly triggering fraud detection. (2) Smart retry timing: analyze historical data — payments from a specific bank succeed more on paydays (1st and 15th of month). Time retries accordingly. (3) Card updater: Visa and Mastercard provide a card updater service — automatically update card numbers when a card is re-issued. Reduces “card expired” failures by 40-60%. (4) Dunning emails: send a friendly reminder with a payment update link. A magic link (authenticated, no login required) reduces friction. (5) Grace period: give customers a 14-day grace period in PAST_DUE status before cancelling — many customers update their payment within a week.”
}
},
{
“@type”: “Question”,
“name”: “How do you handle taxes across different jurisdictions for SaaS subscriptions?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Sales tax for SaaS varies dramatically by jurisdiction: US states have different rates and nexus rules; EU VAT applies where the customer is located (not the seller) for digital services; some countries have no tax on software. Approach: never hardcode tax rates — they change frequently. Use a tax calculation service (Avalara, TaxJar, Stripe Tax). On invoice creation: call the tax service with customer address, product type, and amount. The service returns the applicable tax rate and the computed tax amount. Store the tax breakdown on the InvoiceItem. For VAT: collect the customer’s VAT number and store it on their account. Apply reverse charge mechanism for B2B EU transactions (no VAT charged, customer self-reports). For US nexus: you only collect sales tax in states where you have “economic nexus” (typically > $100K in sales or > 200 transactions per state per year). Track nexus exposure per state.”
}
},
{
“@type”: “Question”,
“name”: “How do you implement annual plan billing with a monthly equivalent display price?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Annual plans are often sold as “$10/month billed annually” ($120/year). The billing cycle is annual but the displayed price is monthly. Implementation: store the plan with billing_cycle=ANNUAL and price_cents=12000 (total annual price). Display: price_cents / 12 = $10/month equivalent. On subscription creation: charge the full $120 immediately. Create an invoice for $120, covering the entire year. current_period_end = current_period_start + 365 days. Proration for downgrades: if the customer cancels mid-year and is due a refund: refund_amount = plan_price * (days_remaining / 365). Store period_start and period_end on the subscription for accurate proration calculations. Renewal reminder: send an email 30 days before the annual renewal date. Many customers forget they signed up annually — this reduces dispute chargebacks at renewal.”
}
},
{
“@type”: “Question”,
“name”: “How do you design a metered billing system that handles high-frequency usage events?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Usage events (API calls, storage reads, message sends) can arrive at millions per second. You cannot query the raw events table to compute each invoice — too slow. Two-layer approach: (1) Real-time aggregation: ingest usage events into a Kafka topic. A stream processor (Flink or Kafka Streams) aggregates events by (subscription_id, metric, hour). Write hourly rollups to a metering_rollups table: (subscription_id, metric, hour, total_quantity). This reduces millions of raw events to thousands of hourly summaries. (2) Invoice generation: at period end, SELECT SUM(total_quantity) FROM metering_rollups WHERE subscription_id=X AND metric=Y AND hour BETWEEN period_start AND period_end. Apply pricing tiers. Create InvoiceItems per tier. Real-time usage API: customers can query their current usage: SELECT SUM(total_quantity) FROM metering_rollups WHERE subscription_id=X AND hour >= period_start. Respond in < 100ms."
}
}
]
}

Asked at: Stripe Interview Guide

Asked at: Shopify Interview Guide

Asked at: Netflix Interview Guide

Asked at: Atlassian Interview Guide

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

Scroll to Top