How to Build a Subscription Billing System That Scales
Building a subscription billing system is one of the most critical technical challenges for SaaS founders. Get it wrong, and you'll face revenue leakage, compliance nightmares, and angry customers. Get it right, and you have the foundation for scalable growth.
Key Takeaways
- Start simple: Use Stripe or a dedicated billing platform for MVP, don't build from scratch
- Handle edge cases: Proration, failed payments, and upgrades/downgrades are where most systems break
- Design for scale: Idempotency, retry logic, and async processing are non-negotiable
- Compliance matters: PCI-DSS, tax calculation, and multi-currency support become critical at scale
- Revenue recognition: ASC 606/IFRS 15 compliance is required once you hit significant revenue
The Subscription Billing Landscape
At Axiosware, we've helped launch 24+ SaaS products, and subscription billing is the one area where we consistently recommend: don't reinvent the wheel. The complexity of handling recurring payments, tax calculations, and compliance requirements is far greater than most founders anticipate.
However, understanding how these systems work is critical. Whether you're building on top of Stripe, building your own processor, or evaluating billing platforms, you need to know what you're working with.
Core Components of a Billing System
The Architecture
Subscription Engine: Manages subscription lifecycle (create, update, cancel, pause)
Payment Processing: Handles card transactions, tokenization, and PCI compliance
Invoice Generation: Creates and sends invoices with line items, taxes, and discounts
Dunning Management: Automated retry logic for failed payments
Revenue Recognition: Prorates revenue over subscription periods for accounting
Common Pitfalls That Break Billing Systems
We've seen billing systems fail in predictable ways. Here are the most common issues:
1. Race Conditions on Concurrent Updates
When a user upgrades their plan while a payment is processing, you can end up with inconsistent states. The solution is idempotency keys and proper transaction isolation.
Example: Idempotency in Practice
// Client sends idempotency key with each mutation
const response = await stripe.subscriptions.update(
subscriptionId,
{ plan: 'pro' },
{ idempotencyKey: 'upgrade-pro-12345' }
);
// Server validates idempotency before processing
async function handleSubscriptionUpdate(idempotencyKey, update) {
const existing = await db.query.idempotency.find({ key: idempotencyKey });
if (existing) return existing.result;
const result = await processUpdate(update);
await db.query.idempotency.create({ key: idempotencyKey, result });
return result;
}
2. Proration Math Errors
When a user upgrades mid-cycle, you need to calculate:
- Refund for unused time on old plan
- Charge for remaining time on new plan
- Handle taxes on both amounts
- Ensure the net charge is correct
3. Failed Payment Handling
Failed payments are inevitable. The question is: how do you handle them?
Dunning Strategy That Works
Day 0: Immediate retry with same payment method
Day 3: Email customer, offer updated payment method
Day 7: Second retry attempt
Day 14: Email with cancellation warning
Day 30: Suspend service, final retry
Day 45: Cancel subscription, mark as churned
Building Your SaaS Billing System
Let's walk through the architecture for a production-ready subscription billing system.
Database Schema Design
Core Tables
-- Products and pricing
CREATE TABLE products (
id UUID PRIMARY KEY,
name VARCHAR(255),
description TEXT,
active BOOLEAN DEFAULT true
);
CREATE TABLE prices (
id UUID PRIMARY KEY,
product_id UUID REFERENCES products(id),
type VARCHAR(50), -- 'recurring' or 'one-time'
amount BIGINT, -- in smallest currency unit
currency VARCHAR(3),
interval VARCHAR(20), -- 'month', 'year'
interval_count INTEGER
);
-- Customer subscriptions
CREATE TABLE subscriptions (
id UUID PRIMARY KEY,
customer_id UUID REFERENCES customers(id),
status VARCHAR(50), -- 'active', 'canceled', 'past_due'
current_period_start TIMESTAMP,
current_period_end TIMESTAMP,
cancel_at_period_end BOOLEAN,
created_at TIMESTAMP DEFAULT NOW()
);
-- Payment attempts for audit trail
CREATE TABLE payment_attempts (
id UUID PRIMARY KEY,
subscription_id UUID REFERENCES subscriptions(id),
amount BIGINT,
status VARCHAR(50), -- 'succeeded', 'failed', 'pending'
failure_reason TEXT,
attempt_number INTEGER,
created_at TIMESTAMP DEFAULT NOW()
);
Handling Recurring Payments
The billing engine needs to run continuously, processing subscriptions that are due for renewal. Here's how we structure this:
Async Job Processing
import { scheduleJob } from 'node-schedule';
// Run every hour to process due subscriptions
scheduleJob('0 * * * *', async () => {
const dueSubscriptions = await db.query.subscriptions.find({
where: {
status: 'active',
currentPeriodEnd: { lte: new Date() }
}
});
for (const subscription of dueSubscriptions) {
await processSubscriptionRenewal(subscription);
}
});
async function processSubscriptionRenewal(subscription) {
// 1. Create invoice with line items
const invoice = await createInvoice(subscription);
// 2. Attempt payment
const payment = await processPayment(invoice);
// 3. Update subscription period
await updateSubscriptionPeriod(subscription, payment);
// 4. Handle failure with dunning
if (payment.status === 'failed') {
await triggerDunning(subscription);
}
}
Scaling to Millions in Revenue
Once you're processing significant transaction volume, you need to think about:
Performance Optimization
- Partitioning: Split payment attempts by date for faster queries
- Caching: Cache product/pricing data in Redis
- Async processing: Move invoice generation and email sending to background jobs
- Database indexing: Index customer_id, status, and date fields
Multi-Currency and Tax
As you expand globally, you'll need:
- Real-time currency conversion with rate limiting
- Automated tax calculation (use TaxJar or Avalara)
- VAT handling for EU customers
- Local payment methods (SEPA, iDEAL, etc.)
Case Study: Scaling a SaaS Billing Platform
Case Study: Michigan Sprinter Center
A vehicle dealership needed a custom subscription system for their maintenance packages. We built a SaaS billing system that handles:
- 4,000+ active subscriptions across multiple service tiers
- Automated renewal with smart retry logic
- Multi-location support for their 3 dealership locations
- Revenue tracking that integrates with their accounting software
The result: $185K in first quarter revenue from subscription services alone, with 99.7% payment success rate after dunning optimization.
Case Study: Isla Hotel Reservation Platform
For a hotel management SaaS, we implemented a subscription billing system with:
- Usage-based billing for API calls and bookings
- Seamless upgrades with automatic proration
- Multi-tenant architecture with isolated billing data
This enabled 68% reduction in front-desk calls and supported growth from 10 to 200+ hotels on the platform.
Compliance and Security Considerations
When handling payments, you're dealing with sensitive data. Here's what you need to know:
PCI-DSS Compliance
You have two options:
- Full PCI compliance: You store card data (extremely difficult, requires SAQ-D)
- Tokenization: Use Stripe, Braintree, or similar (highly recommended)
Revenue Recognition (ASC 606/IFRS 15)
Once you hit significant revenue, you need to recognize revenue over time, not when you collect it. This means:
- Tracking deferred revenue in real-time
- Handling refunds and adjustments correctly
- Generating compliance reports for auditors
When to Use a Billing Platform vs. Build Custom
Use Stripe/Paddle When:
- You're launching an MVP or pre-seed startup
- You need to ship in 4-8 weeks
- You have standard subscription models
- Team size is under 10 engineers
Build Custom When:
- You have complex pricing models (marketplace, usage-based at scale)
- You need custom payment flows (offline payments, net terms)
- You're processing $10M+ ARR and need to reduce processing costs
- You have dedicated finance/engineering resources
The Bottom Line
Building a subscription billing system that scales requires careful attention to edge cases, proper async processing, and compliance from day one. The good news: you don't need to build everything from scratch.
At Axiosware, we help founders make the right choice based on their stage and needs. Whether that means leveraging Stripe for an MVP or building a custom SaaS billing system for complex requirements, we've seen it all and can guide you to the right architecture.
Ready to Build?
Whether you need a recurring payments system for your SaaS or a complete billing platform from scratch, our team can help you ship faster and scale smarter.
Start a ProjectWant to see more examples of how we've built billing systems for other companies? Check out our case studies or learn about our SaaS development services.
Tags
Want More Engineering Insights?
Get startup architecture patterns, AI development techniques, and product launch strategies delivered to your inbox.
Join the Axiosware Newsletter
Weekly insights for founders and technical leaders
We respect your privacy. Unsubscribe at any time.
