Stripe Just Wants a Number

Stripe Just Wants a Number

Stripe 只想要一个数字

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. 如果你去问任何一家科技公司的工程师:“你最喜欢处理技术栈的哪一部分?”我敢保证他们的答案绝对不会和我的一样:计费(billing)。根据我的经验,当计费逻辑与常规业务逻辑纠缠在一起时,计费就会变得非常棘手。最终,你的代码会渗透到每一个需要执行计费操作的热点路径中。即便有了大语言模型(LLM),也很难避免代码到处蔓延。

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. 初创公司承担不起这种后果,因为它会导致脆弱且难以变更的定价结构——而没有哪家初创公司有时间去重写计费系统。相反,我们应该让产品事件去通知计费系统:“发生了某些变化,你可能需要为此收费。”我称这些为“可计费事实”(billable facts)。在 exe,我的目标是确保任何人都能处理计费工作,而当出现奇怪的特殊情况时,我再介入。我们无法承受让一个人掌握所有计费信息并将其作为唯一工作重点的风险。

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. Exe 不进行代码审查,这意味着我们编写代码的方式与众不同。我们每个人都有权修改系统中的任何代码。这种模式的权衡在于,它就像拥有一辆车:任何人都可以自己动手完成大部分维护工作。但一旦气缸垫坏了,你就得面对引擎和数百个零件。而我,就是那个负责修理气缸垫的人。在计费系统的早期,我们采用了常规做法:编写一个庞大的函数,它处理所有事情,并产生计费副作用。

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. 为团队增加席位就是一个例子:团队邀请某人以特定角色加入。用户接受邀请、验证账户并加入团队。用户获得团队共享虚拟机的访问权限。用户根据团队的套餐被分配一定数量的计算资源。新用户可以使用 exe.dev。我们通过一系列类似于 Pee-wee 早餐机(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? 最初的席位计费实现确实奏效了。在这些数据库变更(封装在事务中)的过程中,我们同时向计费服务商发出了 API 调用。任何写过计费代码的人可能都这样做过。但随之而来的就是常见的问题:如果 API 调用失败了怎么办?如果数据库事务失败了怎么办?如果团队处于某种奇怪的订阅状态怎么办?如果扣除席位费用时支付被拒绝了怎么办?

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. 我知道这会变得越来越脆弱——堆积如山的计费边缘情况。与其发布后就草草了事(像我过去可能做的那样),我决定换个角度思考这个问题。与其让计费 API 调用散落在代码各处,不如在某些状态发生变化时发布“可计费事实”,并在新事实确定后进行计费对账?“可计费事实”是表明某事已发生变化的原子操作。

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. 利用这些事实,我们可以执行任何业务逻辑,确定资源的新状态,最后再与计费服务商进行对账。毕竟,Stripe 只想要一个数字。它并不关心我们是如何得到这个数字的。这是新的产品流程:团队邀请某人以特定角色加入。用户接受邀请、验证账户并加入团队。用户获得团队共享虚拟机的访问权限。用户根据团队的套餐被分配一定数量的计算资源。新用户可以使用 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. 而在计费端,它是这样工作的:在邀请流程中,一旦邀请被接受,团队席位状态就会被标记为“脏”(dirty)。下游的一个工作进程(worker)检测到该团队被标记为脏。该进程根据业务规则计算席位增量。如果增量发生变化,该进程就会更新 Stripe 中的订阅数量。

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. 有了这种架构,引入新团队成员的过程就不再依赖于我们的计费代码了。如果我们想改变席位增量的计算方式,它不会对添加新团队成员产生任何影响。随着 exe 的持续增长,这种计费模式表现出了良好的扩展性。同样的对账流程也适用于我们所有的计量计费。我们的系统会发布关于活跃虚拟机(如磁盘使用量)的可计费事实,计量工作进程则负责与计费服务商进行对账。

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. 即使我们增加了不同的用户计费方式,这些事实也不会改变。相反,工作重点转移到了如何将这些事实与各种 API 进行对账上。我们在 iOS 应用中已经看到了这种好处:它只需告诉我们的系统“有人通过应用内购买进行了订阅”,后续的事情就会自动对账。计费在这里正逐渐变得平易近人。就像我们其他的代码一样,我们的计费架构赋予了人们按需进行修改的灵活性。我不必担心因为有人决定重写邀请逻辑而导致计费系统崩溃。不用大家去敲“Bryan GPT”来处理计费任务,这种感觉真好。