Back Original

Avoiding n+1 with the expand pattern

You might have heard of the n+1 problem in API design. I have taken GraphQL for granted to avoid that problem in the past (and the present).

Today, I came across a way to avoid it in REST APIs while working with the Stripe API. I am naming this the Expand pattern (courtesy of Stripe API Developers). Take this pattern and implement it in your REST APIs to avoid the n+1 problem.

Problem

Let us look at a simple API call.

1
GET /v1/invoices/:invoice-id
1
{

You usually want to make another request for a field whose ID is present in the response. Let us say we want the customer’s email address. Then we end up making one more API call to get the full customer object.

This does not look bad for a single invoice. But we usually list things.

1
GET /v1/invoices
1
{

To show the customer’s email for each invoice, we make one more call per invoice. List N invoices and we make N more calls - 1 for the list and N for the customers. That is the n+1 problem.

Expand Pattern

Stripe provides a param called expand for this on most of its APIs. You can use it to pass the field names that you want to expand. That means you get the full object instead of just the object id.

1
GET /v1/invoices/:invoice-id?expand[]=customer
1
{

The same works on a list. Expand data.customer and every customer comes back inlined in that single call.

1
GET /v1/invoices?expand[]=data.customer

No more N extra calls.

GraphQL, when?

What if I only want the customer’s email and not the whole customer object?

That doesn’t seem to be possible with expand. It returns all the fields of the expanded object.

This is the place where we enter the GraphQL territory. GraphQL lets you pick exactly the fields you want. expand only saves you the extra round trip, not the over-fetching cost. But still a good win!

Nested Expand

It seems like you can do nested expands in the Stripe API like this

1
GET /v1/invoices/:invoice-id?expand[]=customer.default_source
1
{

However, there is a limitation for how deep you can traverse - 4 levels. Still, a deeply nested fetch that would have been several round trips collapses into one call.

There is no particular reason mentioned for picking 4 as the limit, but if we were to implement the expand pattern in our APIs, we can do something like this. We want a default max depth so a single request cannot fan out forever:

1
Let T = number of tables

No worries

I think this pattern is useful to know about to avoid overthinking things, like “what if my app becomes a hit overnight and it’s doing a lot of n+1 calls”, “That won’t scale!” etc.

I am never going to worry about anything like that while building new apps. Just focus on a solid REST API and do the expand pattern - That should give us good mileage.