Stripe Made The Obvious Choice When Building Its Payments API. It Took Two Years To Fix It.
Going from a single Charge object to represent a payment to a dual PaymentIntent/PaymentMethod was a feat of engineering, and a lesson on how to be ready to throw everything out, and start over.
Stripe spent 2 years rewriting their payments API. They realized they had made a big mistake, but had the deep pockets to fix it.
But for the rest of us, a rewrite is a luxury. We must get it right the first time.
Let me ask you this: What do most people say when they talk about payments? They could say that merchants “charge” customers, right?
That’s what Stripe did in the beginning. They modeled their API on plain English.
Stripe’s API collected credit card details in the form of a Token object. And allowed merchants to create a Charge object using that Token. This is a very common pattern in web applications.
In fact, isn’t this how you would design payments?
Or let me put it this way: isn’t it how Domain Driven Design recommends doing it?
By using the model-based language pervasively and not being satisfied until it flows, we approach a model that is complete and comprehensible, made up of simple elements that combine to express complex ideas.
— Eric Evans, Domain Driven Design
Good engineers pay attention to the way domain experts discuss their field. And they bake that particular language into the software.
Stripe’s engineers are good. That’s why they implemented the Token/Charge architecture.
However, Stripe doesn’t use this architecture anymore.
In this article, I’m going to tell you what they do now instead, and why.
I’m Alvaro Duran, and this is The Payments Engineer Playbook. There’s a ton of resources and tutorials out there that teach you how to build money software. But there’s not much that teaches you how to design this critical software for real users and real money.
It’s not that you can’t Google it. It’s that what you get is often wrong.
And the reason I know this is because I’m part of the Fintech engineering team at Kiwi.com. I’m responsible for what happens when one of our customers clicks the Pay button, which happens 70,000 times a day. And I’m able to see all types of interesting conversations about what works and what doesn't for payment systems behind closed doors.
These conversations are what inspired this newsletter.
In The Payments Engineer Playbook, we investigate the technology that transfers money. And we do that by cutting off one sliver of it and extract tactics from it.
You’ll find a lot of content out there on how to use Stripe’s API.
However, if you want to build payment systems, there isn’t much content out there. Stripe, obviously, isn’t going to help you build one. And so, there’s little about why Stripe’s API was built that way.
The truth is that Stripe's redesign is less simple, but serves a more valuable purpose. Even if it isn’t the simplest route.
Because the simplest route is a mistake.
But first, I want to tell you why Stripe ended up rewriting their API.
From the very beginning, Stripe positioned itself as instant payment processing for developers. In 2017, Bloomberg Businessweek published a feature story on Stripe titled How Two Brothers Turned Seven Lines of Code Into a $9.2 Billion Startup. And even though no one really knows which lines of code the article is referring to, the "7 lines of code" slogan stuck.
A few lines of code were enough to process payments.
Except…that was true only for credit cards.
Stripe's engineers designed the Token/Charge architecture to be very simple. The tradeoff was that it forced you to create the Token first, then the Charge, and be ready to handle the result of the request immediately.
And this didn’t fit payment methods that were slightly different. Like Bitcoin.
Now I don’t think I need to explain to you how Bitcoin works. You already know it doesn’t work like a card payment at all.
First, unlike card payments, Bitcoin payments are initiated by the customer. They are, in the industry parlance, “push” payments. And second, Bitcoin payments don’t finalize immediately. They need 6 blocks for that, or about an hour.
Push payments that don't finalize immediately are cumbersome to integrate in the Token/Charge architecture. They force developers to build webhooks and handle a parallel, asynchronous flow.
OXXO made the problem more salient. OXXO is a Cash on Delivery payment method. And it works the way you would expect: the customer must send cash in a glorified envelope to pay for the goods. Which means that some system will notify you when the envelope arrives.
Until then, you’re in the dark.
First, Bitcoin, then OXXO. The Token/Charge architecture was showing its limitations.
You may be asking yourself "what if we expanded the concept of Token?"
A token gets data from the payer. Why not make it also send customers the appropriate data when it's needed?
That’s exactly what Stripe did!
They created something called Source. A Source object would be specific to each payment method out there. And depending on the payment method, the customer would have to do something…or not.
And that did the trick…for a while.
But the payments industry moves very fast. New payment methods were popping up every few months. And for each one of them, Stripe's engineers had to design a new Source. They couldn't keep up.
And customers couldn't either. In fact, most didn’t even bother. They accepted cards, and hoped it would be enough.
It was a tough pill to swallow, but the engineers at Stripe had to face the truth. The “7 lines of code” architecture had failed them.
What happened?
The mistake was that they assumed that the abstractions built for card payments were universal. Turns out, that was wrong.
Michelle Bu, who was part of the team that redesigned Stripe’s API, put it like this: it was as if they “were trying to build a spaceship by adding parts to a car”.
Before I tell you how they fixed it, I want to explain to you why integrating new payment methods is such a big deal.
Why aren't card payments enough?
Like I’ve said before, a Payment is a Promise made by an Authorized Party about a Transfer. This definition is very useful for software engineers. It combines two concepts we’re all familiar with: Promises and Identity.
We can use that definition to classify all Payment Methods along two axes.
Whether they require customer action or not to identify themselves.
And whether you get an immediate response from the processor or you get a “pending” response.
Card payments, at least the version that doesn’t engage in 3DS, doesn’t require action from the customer, and finalizes immediately.
They’re the simplest payment method. No wonder cards are everywhere.
However, cards are also expensive for merchants. That’s why accepting other forms of payments is starting to become critical. There’s a lot to gain when you can pass some of your payment processing savings to your customers.
Integrating more diverse and localized payment methods is key to this strategy.
Soon after they decided to redesign Stripe's API, Michelle Bu and her team had a breakthrough. Stripe competitors’ card payments API were getting better. But APIs that gave you the ability to integrate any payment method with ease were very rare.
Consistent developer experience had suddenly become
the most important feature of payments API.
The industry had welcomed so many forms of payments, it became clear that Payment Methods were now a first class citizen. And for that reason, they assigned it an object.
In Stripe's latest redesign, a PaymentMethod contains information on “how” a payment gets processed. And parallel to that, a PaymentIntent contains information on “what” is getting paid.
I love this redesign. Even when it took years for Stripe to roll it out.
PaymentIntents ground the consistency of the payment lifecycle, while PaymentMethods provide flexibility. You can add PaymentMethods if you want, remove them if they fail, and refund them if you need to.
What defines PaymentIntents and PaymentMethods is a thread of continuity and identity.
After all, we might have been interpreting DDD wrong. PaymentIntent and PaymentMethod tie nicely to what DDD said about Aggregates:
Each AGGREGATE has a root and a boundary. The boundary defines what is inside the AGGREGATE. The root is a single, specific ENTITY contained in the AGGREGATE.
— Eric Evans, Domain Driven Design
A single object can't represent a Payment. But as an Aggregate of two, Payments work fine.
One for the “what”, which acts as the root of the Payment boundary.
And one for the “how”, which sits inside the Payment boundary. And it's meaningless if it's disconnected to the "what".
I learned a valuable lesson doing the research for this article. And it's this:
Simplicity is a System’s Property.
Payments are deceptive.
Representing a Payment with a single object sounds simple, but that’s because you’re moving the complexity elsewhere. The Token/Charge architecture was the obvious choice, but it made integrating other payment methods way more difficult.
And that could work if all you need is credit card payments. But that is less likely than it used to be.
In time, Stripe's engineers understood that 7 lines of code wasn't everything. They looked at the problem as a whole, and asked how they could make adding new payment methods easier.
That, in the end, is what's making developers more productive. Which was Stripe’s goal all along.
But most companies don't have the expertise, the culture and the impetus to pull off a 2 year rewrite like Stripe did. They can’t afford getting payments wrong.
And as a result, they have two options. They can learn from Stripe's mistakes, and avoid the catastrophe of technical debt. Or they can ignore this lesson, and end up in a state of SNAFU.
This has been The Payments Engineer Playbook. See you next week.