Reading Stripe's Source Code: 7 Patterns Worth Stealing
Stripe's open-source repos and SDKs are a free masterclass. Here's what I copied.
Stripe doesn't open source the core. They do open source the SDKs, the CLI, and a half dozen tools that are, individually, the cleanest production code I've read in years. These are the patterns I now use myself.
I've been reading Stripe's open code on and off for years. Their core platform is closed, but the SDKs, CLI, terraform provider, and various tooling are open. Reading this code regularly has made me a better engineer. These are the seven patterns I've stolen and now use in my own work.
Pattern 1: Idempotency keys as a first-class header
Stripe's idempotency is the cleanest implementation of this pattern I know. Every mutating endpoint accepts an Idempotency-Key header. Server stores the key and the result for 24 hours. Replays return the same response.
The detail worth stealing: they hash the request body and store it alongside the key. If you replay with the same key but different body, you get a 400. That single check has saved me from at least three production bugs.
I now build this into every internal API I write that performs mutations.
Pattern 2: Expandable resources
In their API, you ask for a charge and get the customer ID by default. If you want the full customer object inline, you pass expand=customer. Stripe builds this into the SDK so you can write stripe.charges.retrieve(id, {expand: ["customer", "invoice"]}) and get a fully hydrated object.
This is brilliant because it sidesteps the GraphQL-vs-REST argument entirely. You get the JSON-RPC ergonomics of REST with the over-fetching control of GraphQL, without the schema headache.
I stopped using GraphQL partly because of this pattern.
Pattern 3: Object versioning at the API level
Stripe pins your account to an API version. Every request from your account is interpreted as that version. New customers default to the latest. Old customers stay on the old contract until they upgrade.
This means Stripe can ship breaking changes without breaking anyone. They quite literally never break the API for existing integrations.
For my own work, I copy this for any API that other engineers consume. Pin the version on first request, persist it on the account, and serve that contract for as long as needed.
Pattern 4: Errors are typed, structured, and documented
Every Stripe error has a type, a code, a parameter, a doc URL, and a human-readable message. I get back a JSON object I can switch on. I never parse error strings.
{
"error": {
"type": "card_error",
"code": "card_declined",
"decline_code": "insufficient_funds",
"param": "source",
"doc_url": "https://stripe.com/docs/error-codes/card-declined",
"message": "Your card was declined."
}
}
The doc_url is the killer feature. When I see an error in logs, I click the URL and read the docs. No GitHub issue spelunking. No Stack Overflow.
Pattern 5: Webhook signing with timestamp
Stripe webhook signatures include a timestamp. The signed payload is timestamp + body. Replays older than 5 minutes are rejected. This single design decision eliminates an entire class of replay attacks.
Most webhook implementations I've audited don't include the timestamp. They're all replayable.
Pattern 6: The CLI listens to webhooks locally
The Stripe CLI has a stripe listen command that opens a websocket to Stripe and forwards events to your local dev server. You can iterate on a webhook handler with the same speed you iterate on a REST endpoint.
I now build this into every webhook system I ship. It's not technically complex - a small relay server plus a CLI - but the developer experience improvement is enormous.
Pattern 7: Test mode is a first-class environment, not a flag
Stripe's test mode has its own API keys, its own dashboard, its own data, but the same code paths. You don't conditionally do anything in your application code. The mode is determined by the API key.
Most fintech systems I've audited have if-statements like if (env === "production") chargeRealMoney(). That's a bug waiting to happen. The Stripe approach pushes the mode determination entirely into infrastructure, where it belongs.
Where Stripe falls short
I'm not Stripe-pilled in all directions. Their code generation across SDKs is occasionally inconsistent. Their CLI is faster in Go than the SDKs are in TypeScript. Their pricing pages are an art form, but their actual product pricing is opaque enough that I've billed clients to interpret it.
But on API design and idempotency, they're as good as it gets in the industry.
The sharper insight
The reason Stripe's code is so good is not that they hire better engineers. They hire well, but so does Google and so does Stripe's competition. The difference is that Stripe treats the API contract as the product, with a separate engineering org guarding it. Most companies treat the API as a thin wrapper over the database. That single org structure decision compounds for years.
If you're building an API product, hire an API designer who reports to product, not engineering. Or learn to think like one. The clearest way to learn is to read Stripe.
For more on API design as it applies to fintech specifically, see my piece on fintech architecture patterns.