Overkill GraphQL - Part 1: The Background
Ewan Valentine's brilliant blog series (which I would highly recommend by the way) inspired much of the way I (and Tumelo) currently architect code. I have been drinking the GRPC kool-aid. Now don't get me wrong, it's not all sunshine ☀️ and rainbows 🌈; routing, HTTP2 compatibility and debugging are hard! I've never regretted it though. At Tumelo, we bet big on GRPC from the start and every time we've bet on something else, namely OpenAPI, we've always somewhat regretted it. We use both GRPC and OpenAPI and every time it comes to writing some OpenAPI, boy do I dread it. GRPC though is always fun because:
- It feels like writing interfaces in code rather than YAML
- The code generation provides great type-safety
- The tooling is top-notch
- Google's API design documents give well tested API design patterns
Here's a quick definition of a type and service:
syntax = "proto3";
package ben.person.v1;
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
service PersonService {
rpc CreatePerson(Person) returns (Person) {}
}
How can you not love that! Nowadays, the architecture laid out by Ewan is common: GRPC fronted by OpenAPI. Given I have "grown up" on it, I thought it was time for me to try the new kid on the block: GraphQL. I've never really questioned my appreciation for GRPC and have never really wanted to try something else but it's time for me to do it and I'm going to do it my way:
Complete Overkill & With My Baggage
I hope that this journey helps me and you learn about other communication protocols. In a few posts, I hope to build an architecture that, like Ewan's posts, inspires me and others to improve upon what we have.
Introduction to GraphQL
GraphQL needs no introduction ... That's not true! But equally, I don't want to do an in-depth intro as others have already done this, and have done so much better than I ever could. This is my exploration and journey so here are the bits that I do want to highlight. They are important to me and are a part of why I am doing this:
API first
Having used GRPC extensively, the benefit of separating out the service interface definition from implementation is something I don't want to give up. An API-first approach provides teams with the ability:
- to decide on an interface that is defined in a language that can be common
- to do the high-risk bit upfront (human communication) by agreeing on the contract
- to not need to dive into the other side of the interface
- to work in parallel once the contract has been agreed
- to mock out the contract for testing
- to more easily change out one implementation for another
All these benefits and others are something that I don't believe most people who are building size-able projects should skimp on. So for me, it was a no-brainer: .graphql
definitions up front. No type definitions from code definition. I will write the following first.
type User {
id: ID!
firstName: string!
lastName: string!
}
type Query {
listUsers: [User]
}
Code Generation
Protobufs force code generation onto you and to me the experience has been addictive. Once set up, you are off to the races and the following flow is what I want to keep:
- defining a GRPC endpoint and generating the code
- deploying it straight away because your Go struct wraps the "Unimplemented" version
- fulfilling the interface and testing that function
- deploying it
This flow lowers the barrier of implementing endpoints. It forces you to execute two steps separately: defining and implementing. Once you have your definition, you live in the language, you test your logic, you don't worry that the translation between code and communication protocol will work. So, yep, I will be using code generation from the API first definition, most likely gqlgen.
Microservices
As Tumelo has grown, so have the number of pages and clients we serve. Each have slightly varying requirements that ideally would be served by the exact data they need, hence graphQL. That said, the traditional approach of GRPC services, fronted by a custom graphQL gateway with resolvers pointing to them, never felt very satisfying. The redefining of different layers in different languages just feels wasteful. As such, I have always admired projects like the grpc-gateway as an idea to generate OpenAPI from GRPC. Something I would like to do at Tumelo. A universal interface language seems like a dream.
So when I saw the Netflix talk and series that opened my eyes to "GraphQL federation", my take on GraphQl changed. It didn't just have to be the "front", it could act as the communication language for the whole stack. Why wouldn't the services define their interface with graphQL and then they are grouped together with a smarter gateway? This resonated and I wanted to try it. It opened the door for a true step forward: a universal protocol that can be used internally and externally.
Type-safety & Go
At Tumelo, all of our backend is written in Go. Go isn't perfect, but to me it is the right compromise for this sort of application. It's fast, it's simple, it's fairly opinionated, it's quick to deploy among many other pros. The blend of out-of-the-box security, performance and type-safety means I will be starting with Go.
Credits
- The team at Apollo have pushed GraphQL to new heights. Their federation documentation and their Supergraph Demo for Federation 2 have been huge sources of inspiration.
- The gqlgen docs and project are fantastic and have allowed me to try GraphQL the way I want to in Go.
- The Netflix blog post kicked this whole exploration off.