Why we moved our graphQL server from Node.js to Golang

Roger Chapman
SafetyCulture Engineering
4 min readJun 27, 2018

--

Logo from graph-gophers

In the beginning: Apollo Server

When it comes to building a graphQL server, almost everyone looks to Apollo Server; we were no different. Apollo allowed us to get a graphQL server up and running really quickly. We loved being able to return mock data almost instantly to our mobile/frontend engineers.

Our graphQL server began to grow

At SafetyCulture we have a microservice architecture, so our graphQL server only orchestrates data to our resolvers; no business logic here. However, our graphQL schema grew and grew and became hard to manage.

Magic strings

Most graphQL examples use ES2015 template literals to define the schema, but over time this get hard to read and refactor.

// This looks simple, but what happens when you have 50+ types?
const typeDefs = `
type Query {
hello: String
}
`;

What the arg?

Resolvers also became hard to manage too and new engineers had a hard time figuring out what arguments were being passed to each resolver.

// Resolvers are just one big object, this gets out of control.
const resolvers = {
Query: {
hello: (root, args, context) => {
return 'Hello world!';
},
},
};

Notice root , arg , and context ? What properties do these objects have? This is what a lot of our resolves ended up looking like and confused so many engineers:

// Nobody knows where these args come from!
setSomeValue: (_, { id, myValue }, { userId, someService }) => {
return someService.setSomeValue(userId, id, myValue);
},
Node.js vs Golang minimap

Testing resolvers

In the Node.js world testing resolvers was also hard; managing large objects, the resolver arguments and dependent services made it almost impossible. At least we could still test the actual graphQL queries. Hacker Noon has a great post on this.

JSON mapping

Our internal microservices use gRPC so we already had our messages defined in Protocol Buffers. Apollo Server can auto map object fields to resolvers, so we ended up with a lot of ugly code that would unmarshall the data to JSON and then map to an object so we could auto resolve. Admittedly, we should just fix this problem in the Node.js; it was just a bad pattern, but it was one more thing we could fix with the rebuild.

Go, Go everywhere

Our graphQL server talks to many internal services; all these services are written in Go, so it was also a natural goal to have our edge service also in Go.

GraphQL in Go

For our Golang implementation we used graph-gophers/graphql-go. It has support to parse the GraphQL schema language and give us schema type checking against our resolvers; we can now see if we have a missing resolver at compile time, and not have to debug at runtime to find a typo!

Schema definitions

Instead of long template literals we now define our schema in individual .gql files, this means we can have nicely formatted (easy to read) types using an IDE plugin . Using $ go generate schema and jteeuwen/go-bindata it’s easy to generate our schema during development.

//go:generate go-bindata -ignore=\.go -pkg=schema -o=bindata.go ./...package schemaimport "bytes"// GetRootSchema gets the GraphQL schema from the generated bindata
func GetRootSchema() string {
buf := bytes.Buffer{}
for _, name := range AssetNames() {
b := MustAsset(name)
buf.Write(b)
// Add a newline if the file does not end in a newline.
if len(b) > 0 && b[len(b)-1] != '\n' {
buf.WriteByte('\n')
}
}
return buf.String()
}

Type safe resolvers

Because Golang is type safe all our resolvers had concrete arguments and a simple function that can easily be tested.

func (r *Resolver) SetSomeValue(ctx context.Context, args struct {
ID graphql.ID
MyValue *bool
}) (*MyReturnType, error) {
someService := utils.GetSomeServiceFromContext(ctx)
userID := utils.GetUserIDFromContext(ctx)
return someService.SetSomeValue(userID, string(args.ID), args.MyValue)}
}

Resource utilisation

Memory Utilisation

As a result of deploying the Golang version of our graphQL server there was a significant reduction in memory utilisation within our containers from 55% to only 10%. There was also a drop in CPU utilisation from 30% to under 10%. This means we can adjust our resource reservations in our cluster, in turn reducing the cost of instances needed, thus makes the Foundational Engineering and Finance team happy.

Future

With the move to Golang for our GraphQL server we believe we have a more scalable and manageable service than we had before. The performance increase (thank to goroutines) was also an added bonus.

As SafetyCulture grows we’ll continue to invest in the right tools for the task at hand; Golang and GraphQL are high on our list.

If you’re interested in helping us build something that truly impacts people’s lives and get to do it using the best technologies, we’re always on the lookout for people that are up for a challenge.

--

--

Roger Chapman
SafetyCulture Engineering

Software Engineer helping teams build scalable microservices in Go and gRPC