We’ve all been there. You’re building a brand-new SaaS product, and you need to charge users. Naturally, you grab the Stripe SDK, spin up a quick webhook handler, copy-paste some boilerplate checkout code, and call it a day. For a simple $19/month flat-rate subscription, this works flawlessly. But what happens when your sales team closes a enterprise client who wants custom tiered usage pricing, a minimum spend commitment, and invoices sent on a net-30 schedule?
Suddenly, your clean codebase turns into a spaghetti junction of billing logic. You’re writing custom cron jobs to calculate API usage, manually patching database flags, and praying your billing webhooks don't silently fail. This pain point is exactly why the recent release of LARP (Revenue Infrastructure for Serious Founders) has taken the developer community by storm, sparking a massive conversation about what it actually takes to build and scale robust billing systems.
As developers, we often treat billing as an afterthought—an API integration we can outsource and forget. But as your system grows, revenue infrastructure becomes core application state. Today, we're going to dive deep into why modern billing is a distributed systems problem, how to architect a resilient ledger-based revenue engine, and how to write clean, idempotent code to handle transactions without losing your mind (or your users' money).
The Architecture Nightmare: Why Billing is Hard
At first glance, billing looks like a simple CRUD (Create, Read, Update, Delete) problem. You have a users table, a subscriptions table, and a payments table. When a user upgrades, you change their status to active.
In reality, billing is a highly complex, stateful distributed system that must guarantee absolute data consistency. If your social media app drops a "like" event, nobody dies. If your billing system drops a "metered usage" event, you either lose money or overcharge a customer—both of which are disastrous. Here are the core architectural challenges we have to solve:
- Dual Write Consistency: You have to update your internal database *and* an external payment gateway (like Stripe, Adyen, or Paddle) atomically. If one fails, how do you roll back the other?
- Idempotency: Network requests fail. If a client retries a payment request because of a timeout, you must guarantee they are only charged once.
- Temporal State: Subscriptions aren't static. Users upgrade mid-cycle, downgrade, pause, trial, and stack coupons. Calculating prorations accurately across arbitrary time boundaries is notoriously difficult.
- Idempotent Event Ingestion: For usage-based billing (e.g., charging per million API requests), you must ingest millions of events, aggregate them, and map them to billing cycles without double-counting.
Designing a Ledger-Based Billing Engine
To solve these challenges, serious revenue systems rely on a double-entry ledger architecture. Instead of updating a user's balance or status inline (e.g., UPDATE users SET balance = balance - 10), every financial event is recorded as an immutable transaction log. Your current state is simply the sum of all historical events.
Let's look at how we can model this in a database. Rather than keeping a mutable "credits" column, we use an immutable billing_ledger table:
CREATE TABLE billing_ledger (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL,
amount INT NOT NULL, -- Stored in lowest denomination (e.g., cents)
type VARCHAR(50) NOT NULL, -- 'CHARGE', 'PAYMENT', 'REFUND', 'GRANT'
idempotency_key VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
By enforcing a unique constraint on the idempotency_key, we guarantee at the database level that we can never process the exact same financial transaction twice, even if our application servers retry the operation due to a network glitch.
Building an Idempotent Billing Handler
Let's write a practical Node.js / TypeScript handler that demonstrates how to process a subscription charge safely. We'll use an explicit transaction block to ensure that we only record the transaction in our database if the external payment gateway succeeds, using a two-phase commit-like pattern with idempotency checks.
import { Client } from 'pg';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2023-10-16' });
const db = new Client({ connectionString: process.env.DATABASE_URL });
interface ChargeRequest {
accountId: string;
amountInCents: number;
idempotencyKey: string;
}
export async function processUserCharge(request: ChargeRequest): Promise<boolean> {
const { accountId, amountInCents, idempotencyKey } = request;
await db.connect();
try {
// 1. Start a database transaction
await db.query('BEGIN');
// 2. Check if this idempotency key has already been processed
const existingTx = await db.query(
'SELECT id FROM billing_ledger WHERE idempotency_key = $1',
[idempotencyKey]
);
if (existingTx.rows.length > 0) {
console.log(`Duplicate request detected for key: ${idempotencyKey}. Skipping.`);
await db.query('ROLLBACK');
return true; // Already processed successfully
}
// 3. Make the external call to the payment gateway
// We pass Stripe its own idempotency key to prevent double-charging on their end
const charge = await stripe.charges.create({
amount: amountInCents,
currency: 'usd',
customer: await getStripeCustomerId(accountId),
description: 'SaaS Platform Subscription',
}, {
idempotencyKey: idempotencyKey,
});
if (charge.status !== 'succeeded') {
throw new Error('Payment gateway rejected the charge');
}
// 4. Record the transaction in our immutable ledger
await db.query(
`INSERT INTO billing_ledger (account_id, amount, type, idempotency_key)
VALUES ($1, $2, $3, $4)`,
[accountId, -amountInCents, 'CHARGE', idempotencyKey]
);
// 5. Commit the database transaction
await db.query('COMMIT');
return true;
} catch (error) {
// If anything fails (Stripe timeout, DB crash, etc.), we rollback the local DB state
console.error('Billing failed, rolling back transaction...', error);
await db.query('ROLLBACK');
// Note: In production, you would trigger a reconciliation worker
// to check Stripe's state if the DB rolled back *after* a successful Stripe charge.
return false;
} finally {
await db.end();
}
}
async function getStripeCustomerId(accountId: string): Promise<string> {
// Helper to fetch or create a Stripe customer ID associated with your local account ID
return 'cus_H123456789';
}
Why this pattern matters
In the code above, if the network drops right after Stripe charges the card but before our database can execute COMMIT, our database rolls back. Because we passed the idempotencyKey directly to Stripe, when our background reconciliation system retries this exact request 5 minutes later, Stripe will return the existing charge instead of charging the card again, and our database will safely record the ledger entry.
The Rise of Metered Billing and "Event-Driven" Revenue
As the tech landscape shifts from traditional SaaS seats to AI-driven models (where users pay per LLM token consumed, or per database query executed), the architecture shifts from transactional to event-driven.
If you have thousands of users sending millions of API requests, you cannot hit your billing database or Stripe on every single request. Instead, you need a high-throughput ingestion pipeline:
The flow looks like this:
- Your API gateway processes user requests and emits "usage events" to a high-throughput message queue like Apache Kafka or RabbitMQ.
- A real-time stream processing engine (like Apache Flink, or a lightweight Redis-based consumer) aggregates these events over a window of time (e.g., hourly).
- The aggregated chunks are written to a time-series database (like ClickHouse or TimescaleDB) optimized for analytics.
- At the end of the billing cycle, your revenue engine queries the time-series database to calculate the final usage invoice.
This decoupling is critical. If your billing provider goes down, your core API gateway doesn't stop processing requests; events simply buffer in your message queue until the billing system recovers.
Wrapping Up: Build, Buy, or Both?
When you're starting out, buying is almost always the right answer. Use tools like Stripe Checkout, Outkeep, or emerging players like LARP to get to market quickly. But as your product scales, understanding the underlying patterns of revenue infrastructure—immutable ledgering, absolute idempotency, and asynchronous event reconciliation—becomes mandatory.
A resilient billing system is more than just a way to collect cash; it's a competitive advantage that allows your sales team to experiment with pricing models, gives your customers transparent usage data, and keeps your engineering team sleeping soundly through the night.
What are you using for your billing stack? Have you ever had to clean up a billing horror story? Let’s chat in the comments below!
If you found this deep dive useful, don't forget to subscribe to the "Coding with Alex" newsletter for weekly breakdowns of infrastructure, system design, and software engineering patterns.