Stripe Just Wants a Number

cbrewster1 pts0 comments

Stripe Just Wants a Number - exe.dev blog

If you went up to an engineer at any tech company and asked “What’s your favorite part of the stack to work on?” I can guarantee their answer wouldn’t align with my own: billing.

In my experience, billing becomes difficult when billing logic gets tangled up with regular business logic. You end up with code touching every hot path that needs to do a billing operation. Even with LLMs, it’s a struggle to not have it sprawl everywhere. Startups can’t afford this because it leads to brittle pricing structures that are hard to change—and no startup can allocate time to rewrite billing. Instead, we should have product events tell the billing system “something changed and you may need to charge for it.” I call these billable facts.

Here at exe, my goal is to make sure that anyone can work on billing and when the weird idiosyncrasies show up, I can step in. We can’t afford to have one person hold all the billing information and for that to be their sole focus. Exe doesn’t do code reviews, which means we have a different approach to writing code. We all have the right to change any code in our system. The trade-off is it’s like owning a car: anyone can DIY the majority of the work. Once a head gasket blows, you’re dealing with the engine and hundreds of parts. I’m the one who has to fix the head gasket.

In our earliest days of billing, we did the usual approach: some giant function that does all the things and has a billing side effect. Adding a seat to a team was one such example:

The team invites someone to join in a specific role.

The user accepts the invitation, verifies their account, and joins the team.

The user gains access to the team’s shared VMs.

The user gets allocated a certain amount of compute resources based on their team’s plan.

The new user can use exe.dev.

We figure out how to charge for this seat through a series of handwaving akin to Pee-wee’s breakfast machine.

The first implementation of billing for seats worked. In the middle of these database changes (wrapped in a transaction), we also issued billing API calls to our provider. Anyone who has written billing code before has probably done this. But the usual questions present themselves:

What happens if the API call fails?

What happens if the database transaction fails?

What happens if the team has a weird subscription state?

What happens if the payment is declined when charging for the seat?

I knew this would become increasingly brittle—a pileup of billing edge cases.

Rather than shipping it and calling it done (like I might have done in a past life), I decided to think about the problem differently. Instead of having billing API calls littered in the code, what if we issued billable facts about the state of something and reconciled billing once the new facts were settled? Billable facts are atomic operations that indicate something has changed. Using the facts, we could perform any business logic, establish the new state of a resource, and then finally reconcile the state with our billing provider. After all, Stripe just wants a number. It doesn’t care how we get there.

This is the new product flow:

The team invites someone to join in a specific role.

The user accepts the invitation, verifies their account, and joins the team.

The user gains access to the team’s shared VMs.

The user gets allocated a certain amount of compute resources based on their team’s plan.

The new user can use exe.dev.

And this is how it works on the billing side:

During the invitation flow, the team seat state is flagged as dirty after the invitation is accepted.

A downstream worker notices the team is flagged as dirty.

The worker calculates a seat delta based on business rules.

The worker updates the subscription quantity in Stripe if the delta changed.

With this architecture, onboarding a new team member is no longer dependent on our billing code. If we wanted to change how the seat delta is calculated, it would have no impact on adding new team members.

As exe continues to grow, this billing pattern has been scaling well. The same reconciliation process runs for all our metered billing. Our system issues billable facts about active VMs, like disk usage, and metering workers reconcile with billing providers. Even as we add different ways to bill users, these facts don’t change. Instead, the effort shifts towards figuring out how to reconcile facts with various APIs. We’ve seen the benefits with our iOS app, which just tells our system “someone subscribed with an in-app purchase” and things reconcile after the fact.

Billing is slowly becoming approachable for others here. Like the rest of our code, our billing architecture has given people the flexibility to make changes as they see fit. I don’t have to worry about billing breaking because someone decides to rewrite how invites work. It’s nice to not have folks tap “Bryan GPT” to do a billing task.

billing team code facts user seat

Related Articles