SDSystem design
Start learning free

LESSON 129 OF 140 · PRACTICE PROBLEMS

Subscription Billing

Read the written walkthrough

SYSTEM DESIGN · INTERVIEW WALKTHROUGH

Design a Subscription Billing System: System Design Interview Practice

The problem

"Design the billing behind monthly subscriptions." Basic costs $30 a month and Pro $60, new customers get a 14-day free trial, and anyone can change plans at any time. Nobody presses Pay: the system charges each renewal itself, on its date.

It reuses the Payment System's pattern, on a schedule: every charge still carries an idempotency key and lands in a ledger. What's new is everything around the charge: renewals on their dates, trials that turn paid, failed cards, plan changes in the middle of a month, and invoices that never change once sent. Park billing by usage, such as per API call.

The numbers

From the interviewer: 60 million subscriptions, each billed once a month on the day it started. Take a month as 30 days and a day as about 100,000 seconds (86,400 really).

  • Renewals: 60 million ÷ 30 = 2 million a day, every day, about 20 a second (about 23 with 86,400).
  • Failed cards: say 1 renewal in 20 is declined on the first try: about 100,000 a day.

The load is small. What's hard is correctness: every renewal must happen, once, on its day.

Renewals: one invoice per period

record         fields
subscription   id, customer, plan, renews_on, status
invoice        id, subscription, period, lines, status
               unique (subscription, period)

Each night a job finds the subscriptions due, with an index on renews_on as in the Job Scheduler, and for each one creates the period's invoice and charges it. The unique key of subscription + period is what makes a renewal happen once. Say run 1 crashes just after charging s82, and run 2 starts again from the top:

invoice     run 1                    run 2
s81 · Nov   created, charged, paid   paid: skipped
s82 · Nov   created, charged, crash  open: try in82-1 sent again, paid
s83 · Nov   not reached              created, charged, paid
s84 · Nov   not reached              created, charged, paid

Each try to charge an invoice carries a key made of the invoice and the try, so run 2's try for s82 gets back the charge run 1 made: four cards, four charges. Keying charges by the job's run id instead charges s81 and s82 again in run 2, six charges. Moving the due date after each charge leaves a gap: s82's date never moved, so it's charged twice.

Free trials: a free first period

A trial is a subscription whose first period costs nothing and ends on day 14. Its due date is the trial's end, so the same renewal job charges it then, with the same unique key, and a cancel before that means nothing is due. Ana gives her card at sign-up; the provider checks it and keeps it for later. Say an email reminds her 3 days before the first charge: good practice, and some places' laws require it. A separate "convert trials" job would need its own once-only key, retries and cancel checks; charging at sign-up and refunding cancels turns a free trial into a charge on day 1.

Failed cards: retry, then cancel

Many declines don't last: money comes in on payday, or the customer adds a new card. So the system retries and asks for help, a process called dunning:

day   what happens
0     declined; email Ana
1     try: declined
3     try: declined
5     try: declined
7     last try fails: cancel

Each retry is a due date on the open invoice, so the nightly job picks it up like a renewal, and the invoice can be paid only once. A retry after a known decline is a new try with a new key (in82-2). Canceling at once loses customers who meant to pay; retrying every hour is 168 tries a week, which banks treat as a warning sign and card networks limit.

Plan changes: prorating

Ana paid $30 for this month of Basic. On day 20 of 30, with 10 days left, she upgrades to Pro. Basic is $1 a day and Pro $2, so she gets a credit for the unused days and a charge for the new plan's:

line                    amount
unused Basic, 10 days   −$10
Pro, 10 days            $20
due today               $10

On her usual date she pays the full $60. Charging $30, the difference for a whole month, bills 20 days that are over; $60, a full Pro month, makes her pay twice for the last 10. A downgrade works the other way: the credit is bigger than the charge and waits for her next invoice.

Invoices never change

A sent invoice is a record the customer may have paid and filed, and tax rules in many countries require it to stay as sent, with numbers that run without gaps. Say invoice in81, $60 for November, missed a $10 coupon:

document          line             amount
invoice in81      Pro, November    $60
credit note cn5   coupon missed    −$10 (refers to in81)

A credit note that refers to in81, or a −$10 line on the next invoice, fixes it. Editing in81's total, deleting it, or resending a quietly fixed PDF leaves two versions of one invoice and no record of why.

Same pattern as the Payment System

Every charge is still the Payment System's: an idempotency key per try, a timeout treated as unknown, statuses that only move forward, and a nightly reconciliation against the provider's report. The ledger rule carries over too: mistakes are fixed with new entries, never edits.

Summary

  • Scope: renewals, trials, failed cards, plan changes, invoices.
  • Data: a due date per subscription; one invoice per subscription and period.
  • Numbers: about 2 million renewals a day, 20 a second.
  • Services: one renewal job for renewals, trials and retries.
  • Scale: a unique key of subscription + period, so a rerun charges once.
  • Failed cards: retry on days 1, 3, 5 and 7, email, then cancel.
  • Plan changes: credit the unused days, charge the new plan's.
  • Invoices: credit notes and new lines, never edits.
Keep going with system designInteractive lessons from the building blocks to Twitter, Uber and payments, the way interviews ask. Free to start.
Start learning free