I built a Stripe-based billing system
Sat, Jul 4 2026
I am at the end of an intense ~4 months of re-building a self-serve billing system backed by the Stripe API at my employer. We flipped on the feature flag a few days ago, making it available for new customers. A few random tidbits of knowledge I collected along the way:
Parse and validate the Stripe subscription object
The Stripe subscription API returns json data representing the state of the subscription at that moment. It is likely your integration only considers a subset of subscription states valid. Make that explicit by parsing and validating this json into your own Subscription object. Design the rest of the code to use this internal subscription object. Make it such that it is easy to create an in-memory representation of your customer that has all the information necessary to understand the state they are in.
Functional core, imperative shell
The plan update path is the most involved part of our integration. Instead of discrete plans for a customer to pick from, we allow more fine-grained customization to the features within the plan. Each feature has it's own pricing curve. The pricing of some features depends on the cost of others. The customer can also come back and update their selection mid-cycle. Some changes apply immediately, others are scheduled for the end of their billing cycle.
So a plan update is non-trivial. One strategy I used to tame this complexity is to only make network requests at the "edges" of the system. At one end, we request the Stripe subscription along with other relevant resources like subscription schedule, product, price and discount objects. Database reads are also done at this point. This bundle of data is passed to the "functional" core. This part of the code only operates on in-memory objects and is responsible for implementing the business rules related to updating a plan. It performs no I/O. It returns a set of data structures that represent the parameters needed to update the Stripe subscription into its desired state. At the other edge of the system, we take these output params, create a Stripe API request and send it.
Since Stripe requests are not randomly strewn around your business logic, you can thoroughly test the most important (and potentially most complex) part of your integration in seconds. This is a huge deal for gaining confidence in changes. Billing code tends to be daunting for newcomers because it moves money. Anything we can do to reduce the barrier to maintaing it is welcome.
Furthermore, you now get "preview" functionality for free. With some work, you can take a description of a change a customer would like to make and preview how that would affect their future state without actually modifying anything in Stripe. I think this is pretty cool.
Try to use Stripe prorations
If a customer makes a plan change in the middle of their cycle, we credit them for unused time, and potentially charge them for the remaining time on the new product. Stripe subscriptions support this. Use it. Don't do what we did. At the time of writing, Stripe's proration logic is mostly all-or-nothing: Time-based proration for all subscription items, or no proration at all. This felt too restrictive to us. So we initially decided to pass "prorations": "none" on all our requests and do manual prorations because we had slightly niche logic for one of our products. Right before we shipped the new system, I spent an evening deleting all the manual proration code I had written and using Stripe's proration logic instead.
I think it's easy to fall into this trap. If it's the first time you are doing it, the proration formula seems easy enough. But it interacts with the rest of your billing decisions in interesting ways. Here are some questions to ask yourself before sinking time into it:
- How bad would it be if you just used Stripe prorations? If it's not a complete deal-breaker for your integration, consider adjusting your business rules so you can use Stripe's default behavior.
- Can your subscriptions have flat dollar discounts? If so, think deeply about how these interact with prorations.
- Do you ever issue a credit to a customer? If so, how do you know how much they previously paid? Imagine a customer paid on the 1st, someone gave them a discount on the 2nd, and they made a plan change on the 3rd that would involve a credit proration. Though their subscription has a discount, the credit should not consider it since they did not have it when you charged them.
- Does your Finance team need proration invoices to look a certain way? A keyword is "revenue recognition".
- Do you have any flows where the customer can update their billing cycle immediately? This would involve a credit for unused time on their old plan and a charge for the new plan. If these are two separate invoices, their order matters. If the credit invoice applies after the charge, the customer might not be able to use that credit till their next invoice, which might not be soon.
Incremental progress
I am not even close to being done with this work. The next chunk of work is migrating old customers to the new system. This will take a while. But it is a big relief to finally merge my 3-month old branch into master. Identifying milestones at which you can ship something is really useful. It is unlikely you'll be able support all possible customer states in one go. Whenever I think I finally understand how things work, Finance surprises me yet again with an example of a weird customer we sold a weird plan to this one time. Release something that works for an initial batch of users, and slowly expand from there.