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
- Subscription creation: User selects a plan. A subscription record is created in TRIALING or ACTIVE state. The first invoice is generated in DRAFT.
- 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.
- Payment success: Invoice moves to PAID. Subscription renews: current_period_start and current_period_end advance by one interval.
- Payment failure: Invoice stays OPEN. Subscription moves to PAST_DUE. Dunning begins.
- 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.
- 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.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What are the main components of a scalable billing system design?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A billing system consists of a usage metering pipeline (event ingestion and aggregation), a rating engine that applies pricing rules to metered usage, an invoicing service that generates line-item invoices at billing cycle boundaries, a payment collection layer that integrates with a payment processor, and a dunning workflow that handles failed payment retries. Supporting components include a product catalog, subscription state machine, and an immutable ledger.”
}
},
{
“@type”: “Question”,
“name”: “How do you handle subscription proration in a billing system?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Proration is calculated by determining the fraction of the billing period remaining at the time of the plan change and applying that fraction to the price difference. For upgrades, the customer is charged for the prorated difference immediately. For downgrades, a credit is applied to the next invoice. The billing system records a proration line item with the effective date, old plan, new plan, and computed amount to maintain a clear audit trail.”
}
},
{
“@type”: “Question”,
“name”: “How does a billing system handle usage-based (metered) pricing at scale?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Usage events are ingested through a high-throughput event pipeline (Kafka or a similar message queue) and written to a time-series or columnar store for aggregation. A rating engine reads pre-aggregated usage at invoice time, applies tiered or per-unit pricing rules, and produces billable amounts. To support real-time usage dashboards, a streaming aggregator (Flink or Spark Streaming) computes running totals with at-least-once delivery and idempotent deduplication.”
}
},
{
“@type”: “Question”,
“name”: “What consistency and reliability guarantees does a billing system require?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Billing systems require exactly-once charge semantics to avoid double-billing, achieved through idempotency keys on every payment request. Invoice generation must be idempotent and versioned so reruns produce the same result. The ledger is append-only with double-entry bookkeeping to ensure credits equal debits. All state transitions (subscription created, invoice generated, payment collected) are recorded as immutable events enabling full audit trails and reconciliation.”
}
}
]
}
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