Feeds with GraphQL (Home Feed — Part 1)

How we built the Whatnot Home Feed using backend driven UI and GraphQL

Rami Khalaf
Whatnot Engineering
8 min readApr 26, 2022

--

Imagine you’re a department store on Black Friday with the best deals on TVs in the country: your doors would need to be indestructible to handle the incoming horde of people maniacally coveting that sweet new TV at a price they can’t refuse. Whatnot’s doors were not indestructible, and they failed any time we promoted a big event on the platform.

Let’s look at the history of the Whatnot home feed and the interaction between clients and backend…

The Whatnot home feed/browse experience was generated by baking the layout into each client. The problems with this approach were:

  • High volume of requests at a high rate starve the available server pool
  • Mobile client releases required for layout and content changes
  • Ongoing mobile client work to support growing number of users and categories

Clients generated feeds based on user preferences such as followed users and categories. Preferences were fetched followed by a separate request for each preference’s details. This worked fine when we first started, but we had a massive year of growth across users and preferences.

We heavily rely on push notifications to relay time sensitive information to users, and the resulting influx of users opening notifications at the same time caused a rush of users opening the app. Each user triggered a large number of requests on open which then led to bottlenecks on the backend when servicing other users. This resulted in a poor product experience: users didn’t know where to find the content they cared about, load times were inconsistent, and new content randomly appeared after screens seemed to have finished loading. Because each app had a different version of the home feed engineers had to update, build, deploy, and release every app when deciding on changes to what users should see when they open the app. The effort it took to get these changes to users included managing divergent release cycles for each app platform, rolling out new builds, and waiting for users to upgrade. Not only did ad hoc app releases get in the way of delivering value to users, they also took invaluable time from our small single digit number engineering team.

The last problem was scaling product categories. When I joined Whatnot we were focused on three categories: Funko, Sports, and Pokemon Cards. Over time we grew the number of categories supported 20x but the existing product wasn’t scalable. As users spent more time in the app, followed more users, watched more streams, and built affinity for more categories, the feed became a mess and the most relevant content for each user was buried. There was too much content for users to get through to get to what they cared about. At the same time, new users who signed up didn’t get easy access to content unless they told us about their category preferences, so they were not seeing what Whatnot had to offer them. Existing users had too much content, new users had no content. Our one size fits all approach to the feed wasn’t working.

We realized we needed a solution that could support our current and future growth plans. That’s where backend driven UI came in.

Backend Driven UI with GraphQL

Our client/server calls are served built using GraphQL in a Python Flask so it was an easy choice given the infrastructure and tooling built into the app. Note: The concepts in this post are not exclusive to GraphQL and can be leveraged with other methods. GraphQL is a choice we made factoring in the trade offs.

One of the main advantages of GraphQL is being able to abstract away complex systems/calls into one query that’s constructed by the caller to exactly the pieces of data necessary. We were not leveraging this advantage by having separate requests. The influx of requests mentioned earlier could all be bundled together into one, solving the request volume problem but not the issue of clients defining the content on pages and how they looked. We abstracted this away by introducing new concepts called Feeds and Sections.

A Feed is a collection of sections, and encapsulates the data and relationships required to describe the sections. A section is a collection of content, and encapsulates how the section and any content entity is displayed. With this schema all clients can build support to handle each section, feed, and display type. The server can generate and sort sections based on the request/user context. With this simple schema we can represent any page on our clients!

We can represent any page on Whatnot as a feed, and can generate a page on the server that contains any content type deemed important for the context. The home page focuses on live streams, but as users navigate and drill deeper into the product we want to show other relevant content like products or subcategories. A Feed is how we support that navigation and change of scenery from one page to the next. A section itself is a content type that can represent different feeds in the app, and allows the user to drill deeper with navigation. That destination is driven by the server dynamically.

Feed breakdown and navigation

With Sections, each one encapsulates the configuration on how the content is displayed. We can represent any collection of content as a Section, and it can be backed by any data source. With this structure we can configure a feed to contain mixed content types, with mixed displays and treatments for entity types. Each client implements a component that can handle Sections, and use GraphQL fragments to define what data is needed to display each entity. The components to render a Livestream are different from a Category’s, so each content type has a dedicated fragment that defines the data needed to render. As Sections get more specialized and new section types are introduced, instead of combining all styles into one component we will have dedicated section components for different section types.

The images below show an example of a feed containing multiple content types with different treatments.

List VS Carousel vs Feed, Tile vs Large Tile

Pagination

The more sections and data sources we added the longer it took to generate the feed for users. Loading the full feed is wasteful, since our goal with the new system is to push the most relevant content to the top of the page. To solve this we introduced pagination with the Relay spec. Relay has a standard structure for how pagination should work, and every client can implement support consistently. The data structure for input and retrieval is opinionated but we can make additions and adjust how the data is generated to meet spec. Relay makes no assumptions on your data sources so we can use the framework to paginate through dynamically generated and static content. With pagination we can scope queries to only the content needed for the initial screen.

Having a structured pagination scheme builds backwards compatibility into the apps with any future changes introduced, and abstracts away the complexity of pagination.

Dynamic Feeds

The core of the Whatnot app is Livestreams. Livestreams are ephemeral content that start and stop at any time. Personalized Livestream results are generated in real time. There are many signals that go into the end result like account age, time, activity, user preferences, promotions, and more. Different feeds serve different purposes.

For new users — when we don’t have insight into preferences — we generate guesses based on what we know. The GraphQL schema allows us to turn any recommendation into a feed that can be displayed on screen. One GraphQL query can serve one of any number of feeds and sections based on eligibility. The feed a logged out user sees is different from what someone in Canada sees. The same is true for a user on an old version of the app compared to the latest. We use a combination of internal systems and our feature flagging system Statsig to add and test new feeds/sections. GraphQL helps us dynamically serve these changes based on different contexts and support backwards compatibility. Making a change to feed configurations on the server propagates to every client within seconds. These changes can happen anywhere from code deployed, feature flag changes, ML model updates, or personalization changes.

Home feed for users in Canada

Release

The changes needed to update the apps to this new scheme were not backwards compatible with the previous app builds. To safely roll out this change we used Statsig to slowly ramp up the new feed system across clients/servers. Everyone on the team was able to dogfood the new experience for some time in the same production builds that users had installed. If any major issues came up we were able to mitigate by gating the update to specific versions, and fix forward. We repeated this process until it was ready then rolled it out to public users.

Post release we iterated and introduced new sections and data sources for personalization and recommendations (which we will explore in the next parts of this series!). One of the sections added had an issue that caused public requests to timeout. Server control allowed us to mitigate this issue by removing the section from feeds in seconds. With the old system we would have needed to hotfix clients to remove the faulty section.

Summary

All together, GraphQL helped us:

  • Reduce the number of requests to one
  • Define a consistent structure for clients to present content on Whatnot
  • Support backwards compatible dynamic feeds
  • Enable product growth by tailoring the experience for different categories/users.

In the next parts of this series we’ll dig into areas further down the stack by looking at how we build the iOS and Android experiences, discovery, real time recommendations, ranking, and data platforms!

We’re just getting started on our backend driven UI journey. Interested in building with us? We’re hiring!

--

--