Low Level Design: Billing System

What Is a Billing System?

A billing system manages the full revenue lifecycle: creating subscription plans, tracking usage, generating invoices, triggering payment collection, and handling failures like dunning. It differs from a raw payment processor in that it owns the business logic around when and how much to charge. Billing systems must be accurate to the cent, auditable, and resilient to payment failures without losing customers unnecessarily.

Data Model


TABLE plans (
  id            UUID PRIMARY KEY,
  name          VARCHAR(128),
  price_cents   INT,
  currency      CHAR(3),
  interval      ENUM('MONTHLY', 'ANNUAL'),
  trial_days    INT DEFAULT 0
);

TABLE subscriptions (
  id            UUID PRIMARY KEY,
  user_id       BIGINT NOT NULL,
  plan_id       UUID REFERENCES plans(id),
  status        ENUM('TRIALING', 'ACTIVE', 'PAST_DUE', 'CANCELED', 'PAUSED'),
  current_period_start TIMESTAMP,
  current_period_end   TIMESTAMP,
  payment_method_id    UUID,
  created_at    TIMESTAMP DEFAULT NOW()
);

TABLE invoices (
  id            UUID PRIMARY KEY,
  subscription_id UUID REFERENCES subscriptions(id),
  amount_cents  INT,
  currency      CHAR(3),
  status        ENUM('DRAFT', 'OPEN', 'PAID', 'UNCOLLECTIBLE', 'VOID'),
  due_date      DATE,
  paid_at       TIMESTAMP,
  created_at    TIMESTAMP DEFAULT NOW()
);

TABLE invoice_line_items (
  id            UUID PRIMARY KEY,
  invoice_id    UUID REFERENCES invoices(id),
  description   TEXT,
  quantity      INT,
  unit_price    INT,
  total_cents   INT
);

TABLE dunning_attempts (
  id            UUID PRIMARY KEY,
  invoice_id    UUID REFERENCES invoices(id),
  attempt_number INT,
  attempted_at  TIMESTAMP,
  result        ENUM('SUCCESS', 'FAILED', 'PENDING')
);

Core Workflow

  1. Subscription creation: User selects a plan. A subscription record is created in TRIALING or ACTIVE state. The first invoice is generated in DRAFT.
  2. Invoice finalization: At the end of the billing period (or after trial), the draft invoice is finalized to OPEN and a payment attempt is triggered via the payment processing system.
  3. Payment success: Invoice moves to PAID. Subscription renews: current_period_start and current_period_end advance by one interval.
  4. Payment failure: Invoice stays OPEN. Subscription moves to PAST_DUE. Dunning begins.
  5. Dunning: Retry attempts are scheduled at increasing intervals (e.g., day 1, day 3, day 7). If all attempts fail, the subscription is canceled and the invoice marked UNCOLLECTIBLE.
  6. Cancellation: User or system cancels. Access is retained until current_period_end; no further invoices are generated.

Failure Handling

Billing jobs must be idempotent. A job that generates invoices must check whether an invoice for the current period already exists before creating one. Using the subscription ID and billing period as a composite unique key on invoices prevents duplicates even if the job runs twice.

Payment retries use the idempotency service: each dunning attempt generates a unique idempotency key so that network timeouts during retry do not cause double charges.

Failed invoice finalization is retried with exponential backoff. The job framework (e.g., Sidekiq, Celery) logs each attempt; alerts fire if an invoice stays in DRAFT beyond its due date.

Scalability Considerations

  • Billing scheduler: A distributed cron (e.g., using a leader-election lock in Redis or Postgres advisory locks) ensures only one instance runs billing jobs per time window, preventing fan-out duplicate charges.
  • Fanout at scale: For millions of subscriptions renewing on the same day, batch the invoice generation job into chunks and publish tasks to a queue (Kafka, SQS). Workers process in parallel.
  • Proration: Mid-cycle plan changes require calculating partial period credits and charges. Store proration line items explicitly on the next invoice rather than adjusting past invoices, preserving audit history.
  • Reporting: Aggregate revenue metrics (MRR, churn) are computed from a read replica or a data warehouse fed by a CDC (Change Data Capture) pipeline, never from the transactional database directly.
  • Multi-currency: Store all amounts in the smallest currency unit (cents, pence). Exchange rates are snapshotted at invoice creation time and stored on the invoice, not recomputed later.

Summary

A billing system layers business logic on top of a payment processor: it owns plans, subscriptions, and invoices, and orchestrates dunning when payments fail. Correctness hinges on idempotent job design, explicit state machines for subscriptions and invoices, and careful use of unique constraints to prevent duplicate billing. At scale, distributed job scheduling and queue-based fanout keep throughput high without sacrificing safety.

See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems

See also: Shopify Interview Guide

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

Scroll to Top