Moving TorchExpo's Apollo GraphQL Server to GQLGen

TorchExpo GraphQL Card

Getting Started

TorchExpo is a web platform built for hosting PyTorch optimized models which can be deployed on Android/iOS phones. At TorchExpo, data is spread out in multiple collections and is of varied type like versions of files and download links, model nomenclature, publishers, details of datsets, tasks and deep learning architectures. A single model card is made of all these details and is associated with atleast one task, architecture, dataset and publisher. I decided to go with the GraphQL due to its simplicity of fetching data for multiple resources and describe my system with several types which ends up describing my database schema (allowing me to fire queries directly on DB and not any REST APIs).
The initial version of GraphQL Server was written using Apollo GraphQL Server in Node.js. While the development took not more than a week, I was able continuously ship it to production due to Heroku’s Git integrations. It was easy to integrate with the React frontend for me, but the /models page of TorchExpo was still slow, even after integrating dataloaders. It took an average of 1000-1200ms to load paginated list of models. This was annoying for me, as TorchExpo is expected to get about to add 100s of new models every month. Given that performance is a priority right now, I wanted to optimize it. For people who don’t know, TorchExpo runs completely on free resources as of now (many thanks to Heroku, Vercel and Fly).

The transition to 99designs/gqlgen

After looking at Apollo GraphQL’s performance even after adding dataloaders and going through some benchmarks, I wanted to give GraphQL in Golang a try. I had two promising options:
- graphql-go which uses runtime type for writing a GraphQL server
- gqlgen which is a schema first approach to write a GraphQL server
Coming from Apollo GraphQL, I liked how schema first approach makes everything easy. To add, this feature comparison by gqlgen finally convinced me to give it a try.

Best part about moving to gqlgen was, all of my schema from previous implementation required 0 line of change. With gqlgen, the code-generation for types, resolvers and other graphql complexity part, made it easy for me to focus on getting the data from my database and just working on logic of resolvers. I was able to port the existing implementation of Apollo GraphQL to gqlgen in a day!

To dive in, gqlgen allows dependency injection which made it easy for me to avoid singletons and move my database and cache connection instances to resolvers easily:

type Resolver struct {
    Config *config.Config
    DB     *database.MongoConnection
    Cache  *cache.RedisConnection
}

Recipes by gqlgen is very useful for integrating with other frameworks like Gin, enabling CORS, implementing Authentication middlewares, etc. I went ahead with the CORS option with default configuration in just few lines:

router := chi.NewRouter()
router.Use(cors.Default().Handler) // use default settings for CORS


Dataloaders

TorchExpo Playground

If you look at a sample query for /models page above, you can quickly see how its going to turn out like:

SELECT id, name FROM tasks WHERE id = ?
SELECT id, name FROM tasks WHERE id = ?
SELECT id, name FROM tasks WHERE id = ?
SELECT id, name FROM tasks WHERE id = ?
SELECT id, name FROM tasks WHERE id = ?

SELECT id, name FROM publishers WHERE id = ?
SELECT id, name FROM publishers WHERE id = ?
SELECT id, name FROM publishers WHERE id = ?
SELECT id, name FROM publishers WHERE id = ?
SELECT id, name FROM publishers WHERE id = ?

SELECT id, name FROM datasets WHERE id = ?
SELECT id, name FROM datasets WHERE id = ?
SELECT id, name FROM datasets WHERE id = ?
SELECT id, name FROM datasets WHERE id = ?
SELECT id, name FROM datasets WHERE id = ?

Whats even worse? most of those models are all owned by the same publisher with same dataset and task! We can do better than this. Luckily, gqlgen has Dataloaders - which again uses codegen techniques for your specific types to generate dataloader middlewares. This makes your queries a bit more fast than usual.

We ended up having following dataloaders in place:

type Loaders struct {
    TaskByID         TaskLoader
    PublisherByID    PublisherLoader
    ArchitectureByID ArchitectureLoader
    DatasetByID      DatasetLoader
}

Integrating dataloader allowed me to tweak the performance a bit and avoid N+1 problem of GraphQL. I will be updating this post soon after doing a thorough benchmarking of both GraphQL servers using wrk. Till then, you can take a glimpse of new TorchExpo Website!

I am becoming seriously allergic to 500-pound websites.