Skip to main content
SaaS Development9 min read

How to Build a Subscription Billing System That Scales

A
Axiosware
Engineering Team

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:

  1. Full PCI compliance: You store card data (extremely difficult, requires SAQ-D)
  2. 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 Project

Want 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

subscription billingSaaS developmentrecurring paymentsStripe integrationbilling architecturepayment processing

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.