Common design patterns at Stripe

Paul Asjes - Jul 14 '23 - - Dev Community

If you haven’t read the previous article on the importance of design patterns, I suggest you start there before reading this one. If however you’re already convinced on design patterns, read on to see my top picks for some examples we use at Stripe.

You might disagree with how the Stripe API is designed, and the design you end up with is likely going to be different than what we use. That’s just fine, since different companies have different use cases. Instead I present here some design patterns that I believe are generic enough to be useful for just about anyone in the API design process.

Language

Naming things is hard. This is true of most things in computer science, and API design is no different. The problem here is that similar to variable and function names, you want API routes, fields and types that are clear, yet concise. This is hard to do at the best of times, but here are a few tips.

Use simple language

This one’s an obvious suggestion, but in practice it’s actually quite hard to do and the cause of much bike-shedding. Try to distill a concept down to its core and don’t be afraid to use a thesaurus for synonyms.

For example, when building a platform don’t confuse the concept of a user and a customer. A user (at least in Stripe terms) is a party that directly uses your platform’s product, a customer (also known as an “end-user”) is the party who ends up purchasing the goods or services that your user might be offering. You don’t have to use those exact terms (‘user’ and ‘end-user’ are perfectly fine), as long as you’re consistent with your language.

Avoid jargon

Industries come with their own jargon; don’t assume that your user knows everything there is to know about your specific industry. As an example, the 16 digit number you see on your credit card is called the Primary Account Number, or PAN for short. When running in fintech circles it’s normal to have people talk about PANs, DPANs and FPANs, so you’d be forgiven for doing something like this in your payments API:

card.pan = 4242424242424242;
Enter fullscreen mode Exit fullscreen mode

Even if you know what PAN stands for, the connection between that and the 16 digit number on a credit card might still not be obvious. Instead avoid the jargon and use something more likely to be understood by a larger audience:

card.number = 4242424242424242;
Enter fullscreen mode Exit fullscreen mode

This is especially relevant when thinking about who the audience of your API is. Likely the core role of the person implementing your API is that of a developer with no knowledge of fintech or any other specialized domain. It’s generally speaking better to assume that people aren’t familiar with the jargon of your industry.

Structure

Prefer enums over booleans

Let’s imagine that we have an API for a subscription model. As part of the API, we want users to be able to determine whether the subscription in question is active or if it has been canceled. It would seem reasonable to have the following interface:

Subscription.canceled={true, false}
Enter fullscreen mode Exit fullscreen mode

This works, but say that some time after implementing the above, we decide to roll out a new feature: pausing subscriptions. A paused subscription means we’re taking a break from taking payments, but the subscription is still active and not canceled. To reflect this we could consider adding a new field:

Subscription.canceled={true, false}
Subscription.paused={true, false}
Enter fullscreen mode Exit fullscreen mode

Now, in order to see the actual status of the subscription, we need to look at two fields rather than one. This also opens us up to more confusion: what if a subscription has canceled: true and paused: true? Can a subscription that has been canceled also be paused? Perhaps we consider that a bug and say that canceled subscriptions must have paused: false.

Does that mean that the canceled subscription can be unpaused?
The problem only gets worse the more fields you add. Rather than being able to check a single value, you need a confusing stack of if/else statements to figure out exactly what’s going on with this subscription.

Instead, let’s consider the following pattern:

Subscription.status={"active", "canceled"}
Enter fullscreen mode Exit fullscreen mode

A single field tells us in plain language what the status of the object is by using enums instead of booleans. Another upside is the extensibility and future-proofing that this technique gives us. If we go back to our previous example of adding a “pause” mechanic, all we need to do is add an additional enum:

Subscription.status={"active", "canceled", "paused"}
Enter fullscreen mode Exit fullscreen mode

We’ve added functionality but kept the complexity of the API at the same baseline whilst also being more descriptive. Should we ever decide to remove the subscription pausing functionality, removing an enum is always going to be easier than removing a field.

This doesn’t mean that you should never use booleans in your API, as there are almost certainly edge cases where they make more sense. Instead I urge you to consider before adding them the future possibility where boolean logic no longer makes sense (e.g., having a third option).

Use nested objects for future extensibility

A follow on from the previous tip: try to logically group fields together. The following:

customer.address = {
  line1: "Main Street 123",
  city: "San Francisco",
  postal_code: "12345"
};
Enter fullscreen mode Exit fullscreen mode

is much cleaner than:

customer.address_line1 = "Main street 123";
customer.address_city = "San Francisco";
customer.address_postal_code: "12345";
Enter fullscreen mode Exit fullscreen mode

The first option makes it much easier to add an additional field later (e.g., a country field if you decide to expand your business to overseas customers) and ensures that your field names don’t get too long. Keeping the top level of your resource nice and clean is not only preferable but soothes the soul as well.

Responses

Return the object type

In most cases, when you make an API call it’s to get or mutate some data. In the latter case the norm is to return a representation of the mutated resource. For example, if you update a customer’s email address, as part of your 200 response, you’d expect a copy of that customer with the new, updated email address.

To make life for developers easier, be explicit in what exactly is being returned. In the Stripe API, we have an object field in the response that makes it abundantly clear what we’re working with. For example, the API route

/v1/customers/:customer/payment_methods/:payment_method
Enter fullscreen mode Exit fullscreen mode

returns a PaymentMethod attached to a specific customer. It should hopefully be obvious from the route that you should expect a PaymentMethod back, but just in case, we include that object field to make sure there can be no confusion:

{
  "id": "pm_123",
  "object": "payment_method",
  "created": 1672217299,
  "customer": "cus_123",
  "livemode": false,
  ...
}
Enter fullscreen mode Exit fullscreen mode

This helps a great deal when sifting through logs or adding some defensive programming to your integration:

if (response.data.object !== 'payment_method') {
  // Not the expected object, bail
  return;
}
Enter fullscreen mode Exit fullscreen mode

Security

Use a permission system

Say you’re working on a new feature for your product dashboard, one that was specifically asked for by a large customer. You’re ready for them to test it out as a beta to get some feedback, so you let them know which route to make requests to and how to use it. The new route isn’t documented anywhere publicly and no one but your customer should even know about it, so you don’t worry too much.

A few weeks later, you push a change to the feature that addresses some of the feedback the large customer gave you, only for you to get a series of angry emails from other users asking why their integration has suddenly broken.

Disaster, it turns out that your secret API route has been leaked. Perhaps that initial customer got so excited about the new feature that they decided to tell their developer friends about it. Or perhaps that customer’s users had a look at their networking panel and saw these requests to an undocumented API and decided that they liked the look of that feature for their own product.

Not only do you have to clean up the current mess, but now your beta feature has effectively been dragged into a launched state. Since making any new changes will now require you to inform every user you have, your development velocity has slowed to a crawl.

Reverse engineering an API isn’t as difficult as you might think it is, and unless you take steps to prevent it, you can assume that people will.

Security through obscurity is the idea that something hidden is therefore secure. Just as this isn’t true for Christmas presents hidden in the closet, this isn’t true with web security. If you want to ensure that your private APIs stay private, make sure they can’t be accessed unless the user has the correct permissions. The easiest way to do this is to have a permission system tied to the API key. If the API key isn’t authorized to use the route, bail early and return an error message with status 403.

Make your IDs unguessable

I touched on this in my Object IDs post, but it’s worth revisiting here. If you are designing an API that returns objects with IDs associated with them, make sure those IDs can’t be guessed or otherwise reverse engineered. If your IDs are simply sequential, then you are at best inadvertently leaking information about your business that you might not want people to know and at worst creating a security incident in waiting.

To illustrate, if I make a purchase on your site and I get confirmation order ID of “10”, then I can make two assumptions:

  1. You don’t have nearly as much business as you probably claim
  2. I might be able to get information about the 9 previous orders (and all future ones) that I shouldn’t be able to, since I know their IDs

For that second assumption, I could try and find out more about your other customers by abusing your API in ways you didn’t intend:

// If the below route isn't behind a permission system, 
// I can guess the ID and get potentially private 
// information on your other customers
curl https://api.example.com/v1/orders/9 

// Response
{
    "id": "9",
    "object": "order",
    "name": "Lady Jessica",
    "email": "jessica@benegesserit.com",
    "address": "1 Palace Street, Caladan"
}
Enter fullscreen mode Exit fullscreen mode

Instead, make your IDs unguessable by for instance using UUIDs. Using what is essentially a string of random numbers and letters as an ID means there’s no way to guess what the next ID looks like based on the one you have.

What you lose in convenience (it’s much easier to talk about “order 42” than “order 123e4567-e89b-12d3-a456-426614174000”) you’ll make up for in security benefits. Don’t forget to make it human readable by adding object prefixes though, generating your order IDs in the format order_3LKQhvGUcADgqoEM3bh6pslE will make your and the humans who build with your API’s lives easier.

Designing APIs for humans

There are many resources out there for how you should design your API, and I hope that this article gave you some food for thought and the incentive to dive deeper into this rabbit hole.

At Stripe we take API design very seriously. Internally we have a design pattern document containing what I’ve written about above and much more. It includes examples of good and bad design, notable exceptions and even a how-to guide on adding things like enums to existing resources. My favorite part is the “Discouraged” section, where examples of questionable design that exist in our API today are highlighted as a warning to future Stripe developers.

If you enjoyed this, check out the other articles in the Designing APIs for humans series. I also recommend joining the APIs you won’t hate community for more thoughts on API design.

About the author

Profile picture of Paul Asjes

Paul Asjes is a Developer Advocate at Stripe where he writes, codes and talks to developers. Outside of work he enjoys brewing beer, making biltong and losing to his son in Mario Kart.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player