Requirements
- Generate invoices for subscriptions and one-time purchases
- Support multiple billing cycles: monthly, annual, usage-based
- Apply discounts, taxes, and proration for mid-cycle changes
- Collect payments via card, ACH, wire transfer
- Handle payment failures: retry logic, dunning (escalating notifications)
- 100M customers, 10M invoices/month
Data Model
Customer(customer_id, name, email, billing_address, tax_id, currency, payment_method_id)
Subscription(subscription_id, customer_id, plan_id, status ENUM(ACTIVE,PAST_DUE,CANCELLED),
current_period_start, current_period_end, billing_cycle ENUM(MONTHLY,ANNUAL),
quantity, discount_id)
Plan(plan_id, name, price_per_unit, currency, billing_interval, usage_based BOOL)
Invoice(invoice_id, customer_id, subscription_id, status ENUM(DRAFT,OPEN,PAID,VOID,UNCOLLECTIBLE),
currency, subtotal, discount_amount, tax_amount, total, due_date, paid_at,
attempt_count, next_retry_at)
InvoiceLineItem(line_item_id, invoice_id, description, quantity, unit_price, amount,
period_start, period_end, type ENUM(SUBSCRIPTION,PRORATION,ONE_TIME,TAX))
Payment(payment_id, invoice_id, amount, currency, method ENUM(CARD,ACH,WIRE),
status ENUM(PENDING,SUCCEEDED,FAILED), processor_id, created_at)
Invoice Generation
Triggered by a scheduled job at billing cycle end. Steps:
- Collect line items: subscription fee for the upcoming period, usage charges for the past period, any proration credits/charges from plan changes
- Apply discounts (PERCENT or FIXED) from the Subscription.discount_id
- Calculate tax: look up tax rate by customer billing address and product type (TaxJar/Avalara API). Tax = (subtotal – discount) * tax_rate
- Create Invoice with status=DRAFT; line items attached
- Finalize to status=OPEN: lock line items, send invoice email to customer
- Attempt payment immediately (for auto-pay) or wait for due date
Proration
When a customer upgrades mid-cycle (e.g., day 15 of a 30-day month): credit unused days of old plan, charge remaining days of new plan.
days_remaining = (period_end - upgrade_date).days # e.g., 15 days_in_period = (period_end - period_start).days # e.g., 30 credit = old_plan_price * (days_remaining / days_in_period) # credit for unused old plan charge = new_plan_price * (days_remaining / days_in_period) # charge for remaining on new plan proration_adjustment = charge - credit
Add proration as InvoiceLineItems: one credit line item (negative amount) and one charge line item, both with type=PRORATION.
Payment Collection and Retry (Dunning)
On first payment attempt failure: update Invoice status=OPEN (not paid), set next_retry_at = NOW() + 3 days. Retry schedule: +3 days, +5 days, +7 days (total 4 attempts over 15 days). On each retry, charge the stored payment method. If all retries fail: mark Invoice status=UNCOLLECTIBLE, Subscription status=PAST_DUE. Send escalating notification emails at each retry: “Payment failed — we’ll retry in 3 days” → “Action required” → “Final notice — subscription will be cancelled.” After dunning period: cancel Subscription, provision access revocation, send final notice.
-- Dunning scheduler query (runs every hour): SELECT invoice_id FROM Invoice WHERE status = 'OPEN' AND attempt_count < 4 AND next_retry_at <= NOW() ORDER BY next_retry_at LIMIT 1000
Idempotency for Payment Processing
Never charge a customer twice for the same invoice. Use idempotency keys when calling the payment processor: key = invoice_id + attempt_count. If the payment API call times out, retry with the same idempotency key — the processor returns the result of the original attempt without creating a duplicate charge. Store processor’s payment_intent_id in the Payment record; check if it exists before creating a new one.
Tax Calculation
Tax rates vary by customer location, product type, and jurisdiction. Use a tax API (TaxJar, Avalara) rather than maintaining a rate table. Call the API at invoice finalization, passing customer address and line items. Cache the tax rate for (country, state, product_type) for 24h — rates rarely change daily. Store the calculated tax as a line item for auditability. For EU VAT: validate VAT IDs for B2B customers (zero-rate if valid EU VAT ID). For US sales tax: nexus rules determine which states require collection.
Key Design Decisions
- Invoice status machine: DRAFT → OPEN → PAID/VOID/UNCOLLECTIBLE — never mutate a finalized invoice, append corrections as credit notes
- Idempotency keys on payment processor calls — prevents double-charges on retry
- Proration as explicit line items — audit trail, customer transparency
- Dunning via scheduled job with next_retry_at — decoupled from payment processor, retryable
- Tax API integration with caching — correct rates without maintaining jurisdiction tables
Stripe system design is the canonical billing and invoice interview topic. See common questions for Stripe interview: billing and invoice system design.
Shopify system design covers subscription billing and invoicing. Review patterns for Shopify interview: subscription billing system design.
Coinbase system design covers payment processing and billing. See design patterns for Coinbase interview: payment and billing system design.