Startup billing systems fail in predictable ways: implementing Stripe webhooks without idempotency, hardcoding plan structures that cannot evolve, and deferring proration logic until a customer complaint forces it. These best practices cover the billing decisions that compound over time — get them right early and the system scales with you; get them wrong and you are rebuilding from scratch at Series B.
Best Practice 1: Use Stripe as Payment Rail, Not as Source of Truth
Stripe is excellent at processing payments. It is not a billing database. Store your subscription state, plan definitions, and customer billing data in your own database and sync with Stripe via webhooks.
Best Practice 2: Design Plans as Data, Not Code
Plan definitions should live in your database, not in application constants. This lets you create custom plans for specific customers, run pricing experiments, and grandfather existing customers on old plans — all without code changes.
Best Practice 3: Implement Webhook Processing with Idempotency and Ordering
Stripe webhooks can arrive out of order, be duplicated, or fail and retry. Your handler must be idempotent and handle out-of-order delivery gracefully.
Best Practice 4: Handle Trial-to-Paid Conversion Properly
The trial-to-paid transition is where most billing bugs live. Handle the three outcomes: successful conversion, payment failure at conversion, and trial expiration without conversion.
Need a second opinion on your saas engineering architecture?
I run free 30-minute strategy calls for engineering teams tackling this exact problem.
Book a Free CallBest Practice 5: Implement Proration from Day One
When a customer upgrades, downgrades, or changes seat count mid-cycle, proration determines how much they owe or are credited. Not handling this correctly leads to billing disputes.
Best Practice 6: Build a Self-Service Billing Portal
Reduce support tickets by giving customers control over their billing.
Anti-Patterns to Avoid
Anti-Pattern 1: Processing Webhooks Synchronously in the Request Handler
Webhook endpoints should acknowledge receipt (200 response) immediately and process events asynchronously via a queue. If your processing takes more than 5 seconds, Stripe retries the webhook, creating duplicate processing.
Anti-Pattern 2: Storing Prices in Application Code
const PRICE_PRO = 49 scattered across components breaks when you change pricing. Store prices in the database, fetch them when needed, and cache aggressively. Even better, let Stripe be the price source and read from the Stripe Price API.
Anti-Pattern 3: Not Handling Subscription Status Changes Atomically
Updating subscription status, feature flags, and usage limits in separate queries creates windows where a cancelled customer still has access. Use database transactions to update all related records atomically.
Anti-Pattern 4: Ignoring Tax Obligations
VAT (EU), GST (AU/NZ/SG), and state sales tax (US) are legal requirements, not nice-to-haves. Use Stripe Tax or a service like Avalara from the start. Retrofitting tax compliance is expensive and requires reprocessing historical invoices.
Startup Billing Checklist
- Stripe webhook handler is idempotent with event deduplication
- Plan definitions stored in database, not hardcoded
- Feature gating reads from subscription plan, not environment variables
- Trial-to-paid conversion handles payment failure gracefully
- Proration logic for mid-cycle plan changes and seat adjustments
- Self-service billing portal (view invoices, update payment method, change plan)
- Dunning emails for failed payments (day 1, 3, 7, 14)
- Subscription status changes update feature access atomically
- Tax calculation integrated (Stripe Tax or equivalent)
- Billing events logged for debugging and audit
- Cancellation flow captures reason and offers retention alternatives
- Invoice PDF generation with company details and line items
- Webhook endpoint returns 200 immediately, processes asynchronously
- Plan limits enforced at the API layer, not just the UI
Conclusion
The billing system you build at seed stage becomes the billing system you operate at Series A. Every shortcut — hardcoded prices, missing proration, synchronous webhook processing — creates technical debt that multiplies with customer count. A customer on a wrong invoice is a support ticket. A hundred customers on wrong invoices is a trust crisis.
Start with Stripe as the payment processor, your database as the source of truth, and idempotent webhook processing as the synchronization layer. Build plan definitions as data, implement proration from day one, and handle trial-to-paid conversion as a first-class flow rather than an afterthought. These decisions cost hours to implement correctly upfront and save weeks of emergency fixes when billing disputes start arriving.
The most impactful best practice is also the simplest: process every webhook idempotently and log every billing state change. When a customer says "I was charged twice" or "my plan didn't upgrade," the answer should take minutes to find in your audit log, not hours of Stripe dashboard spelunking.