ClassPass Admin GraphQL Playground

ClassPass Admin Tooling: A GraphQL Story

Joel Brown
ClassPass Engineering

--

Internal admin tools aren’t always where you’d imagine a focus on tech, but companies of every size have them and they are critical to business success. ClassPass is no different in this regard, and our first admin site was a Django admin app initially created in 2014. It served us well over the years, but eight years on we were starting to feel some pain points. Enter GraphQL. In 2021 we started rewriting our admin app and chose GraphQL to power the backend.

The Pain

Django is a great project and allowed us to spin up an admin site very quickly in 2014 but a lot has changed at ClassPass since then. Architecture-wise, we moved from a monolith to a more distributed microservice architecture. As we split more and more functionality off from the monolith into microservices, we found ourselves having to “stitch” data back together before returning it to the user on the frontend. Doing the stitching in the Django admin framework became very painful as it’s not really designed to solve this kind of problem.

At the same time, we moved to a tech stack based on Kotlin and moved away from python. From a developer experience perspective, ClassPass engineers began to dread working on the Django admin app. We don’t have the deep python or Django expertise that we once had and though our engineers can work on all sorts of tech stacks, our energy now lies elsewhere. Predictably, tech debt began to increase.

It was clear we needed to do something. We decided that any new solution needed to meet two main requirements. 1) The new solution should improve the developer experience and allow for the self-service creation of admin pages/endpoints. 2) We need to make stitching together data from many microservices both easy to do and efficient.

The Solution

To solve the problem of stitching data together we decided to implement a BFF (backend-for-frontend) service sitting behind a react web app. For those not familiar with this pattern, a BFF sits between your frontend and backend microservices and is responsible for fetching data and transforming it into a response that is tailored for your frontend app. For example, your frontend may want to show a user’s page with all their historical payments. A BFF service would fetch data from a user’s microservice and a payments microservice and combine them into a single response.

There are many ways to implement a BFF service. At ClassPass, we have a BFF service for web and mobile apps that is implemented as a REST API. There are also WYSWIG platforms like retool that allow you to do BFF-style data wrangling. In the end, we decided to go with a GraphQL BFF service. GraphQL checked a lot of boxes for us. It allows the FE to define the structure of data returned by the server and most importantly has built-in patterns for efficiently fetching data from disparate data sources. GraphQL implementations exist in many different programming languages, including Kotlin, which is important because we want our engineers working on a stack that they are familiar with.

We chose graphql-kotlin for our GraphQL implementation because it is a native and natural fit for our Kotlin tech stack. All our existing service client libraries are also written in JVM languages, so we were able to utilize these without writing new code. Kotlin is also relatively friendly for frontend engineers dipping their toes into the backend. We wanted to make the developer experience as smooth as possible for all engineers.

What Went Well

A GraphQL query allows a caller to define the shape of the data in the response. By itself, this is a very useful thing. However, the true magic lies in how these queries are resolved. As I mentioned above, GraphQL has a built-in pattern for fetching and stitching data together efficiently called data-loader. The original data-loader implementation was written in JavaScript but there is an implementation in Java as well that works with graphql-kotlin. The data-loader pattern is really a killer feature for GraphQL.

To illustrate its usefulness let’s go back to our example of payments and users. Let’s say we want to display a page of users and for each user, we want to show the amount of their most recent payment. We can do this by first calling our users service to fetch fifty users and then for each user call our payments service to get payment data for each user. If we don’t make those fifty calls to payments service concurrently, we will be in for a long wait. This is where data-loader comes into play. The data-loader pattern handles concurrency for you. Better yet, it also handles batching and caching for even more efficient queries. Yes, you can do all this manually, but GraphQL makes it very straightforward. This is important to us because we want to make it easy for engineers to create new admin pages the right way and not have to fuss with concurrency, batching, and caching.

Room to Improve

There were certainly bumps along the way. One of the things we wanted to optimize for regarding developer experience was increased engineering velocity. While I think engineers are happier working on the more “modern” GraphQL stack I don’t think engineers are moving faster yet. Not all engineers on the team have GraphQL experience, and the technology can be opaque. It has taken engineers a non-trivial amount of time to ramp up on it.

On a more technical note, another challenge for us has been implementing GraphQL mutations. Mutations are what GraphQL uses for operations that modify data. When implementing our admin Graphql service we ported over many CUD (Create, Update, Delete) operations to GraphQL mutations. Unlike GraphQL queries, I don’t think GraphQL mutations have any real benefit over a more typical REST API. In the future, we may consider moving CUD operations to a REST API and let GraphQL do what it does best and just handle queries.

Final Words

Using GraphQL to implement our new admin BFF service has been a success so far. We have also learned a ton along the way. If you find yourself in a spot where implementing a BFF layer makes sense for you then I encourage you to look at GraphQL. I think it is well suited for this use case.

--

--