Our API journey

PayPal’s Checkout products spread across many web and mobile apps, supporting millions of users across ~200 countries and has hundreds of experiments running at any time. These apps leverage the same suite of REST APIs to fetch data needed for building UI.

REST

About 4 years ago, we went all in on REST. Our APIs were pretty clean, small and atomic. Things were great in the beginning. REST has strict design principles that are widely understood. REST is a great way to design and implement APIs for your domain.

However, REST’s principles don’t consider the needs of Web and Mobile apps and their users. This is especially true in an optimized transaction like Checkout. Users want to complete their checkout as fast as possible. If your applications are consuming atomic REST APIs, you’re often making many round trips from the client to the server to fetch data. With Checkout, we’ve found that every round trip costs at least 700ms in network time (at the 99th percentile), not counting the time processing the request on the server. Every round trip results in slower rendering time, more user frustration and lower Checkout conversion. Needless to say, round trips are evil!

Sure, you can build an orchestration API to return all the data you need, but that comes with trade-offs. What do you name it? GET /landing-page? Now this seems like a JSON API, not a REST API. With orchestration APIs, your clients are coupled to your server. Any time you add a new feature or experiment, you add it to your API. Now you’re overfetching, your performance suffers, and users pay the price.

When new requirements come along, developers face a choice: Should we create a new endpoint and have our clients make another request for that data? Or should we overload an existing endpoint with more data?

Developers often choose the 2nd option because it’s easier to add another field and prevent a client-to-server round trip. Over time, this causes your API to become heavy, kludgy, and serve more than a single responsibility.

The start of an orchestration API. Looks great, right?

We started with great intentions. Our API returned user information, a shipping address and funding options. Everything you need to build a Checkout app. Over time, use cases pile up. Every user pays the cost of these fields even if they don’t need them.

Yuck.

Orchestration APIs seem great at first, but we always regret them later.

Bulk REST

REST wasn’t working for us, so we built a Bulk REST API that solves some of these problems. It’s an on-the-fly orchestration API that allows clients to determine the size and shape of the response. Bulk REST allowed us to combine atomic REST requests and reduce round trips.

NOTE: This isn’t to be confused with Batch REST, our external API offering for clients, which is the much better alternative to Bulk REST that we introduced earlier this year.

Here’s an example of a Bulk REST request and response. The request contains a map of REST operations where you specify verbs, endpoints, parameters and dependencies between them.

Bulk REST API Request

Bulk REST API Response

But, it was rarely used.

With Bulk REST, we liked that clients control the size and shape of data, not servers. That freed us from having to tweak an orchestration API every time a client’s requirements changed. We didn’t like that clients had to intimately know how the underlying APIs worked. The request structure was painful enough for developers to not use it. We also wanted to be able to fetch specific fields, not large resources or objects.

Many companies (such as Facebook and Google) that have a Bulk REST offering have the same problem. It’s treated as an optimized, but undocumented feature, not the first-class way to fetch data.

Bulk REST was a step in the right direction, but wasn’t a game changer.

GraphQL