Introduction
Now a days it seems everybody is or wants to do microservices. The endless choice of flavors - gRPC, JSONRPC, GraphQL, etc. - makes it hard to land on a good architectural decision. I've too often seen a microservice turn into a "micro"-lith. I'm not professing to have the right answer for anyone's particular set of constraints - just authoring my experience with GraphQL and giving some of my own insights.
Graph - What is it?
GraphQL is an API specification which shifts query information from REST Query Parameters along many different endpoints to a JSON payload made to a single endpoint. The (claimed) beauty is that it becomes much more ergonomic to request the exact amount of data you need, as opposed to appending request parameters sine fine.
The other added benefit comes with respect to reducing round trips between the server and the client. Instead of making a request for 4 related entities across 4 requests, GraphQL allows you to query the relationships between those entities and return them in one request. This can become incredibly important as you use information from one entity to do a DB lookup (or join) to get particular information.
The final benefit is strong typing - GraphQL enforces you can only send and request particular fields that are defined in your schema. Requesting a non-existent field or sending bogus data will result in an error to the client, often describing what went wrong.
Examples
Before I discuss where GraphQL can fit in the microservice landscape, I think it's important to lay out a traditional scenario with GraphQL - and then show how we can break it up.
The following schema is very popular - it's the example most GraphQL-ers are familiar with:
extend Query {
me: User
topProducts(first: Int = 5): [Product]
}
type User {
id: ID!
name: String
username: String
reviews: [Review]
}
type Review {
id: ID!
body: String
author: User
product: Product
}
type Product {
upc: String!
name: String
weight: Int
price: Int
inStock: Boolean
shippingEstimate: Int
reviews: [Review]
}
Here we are constructing a service which has relationships between Reviews, Products, and Accounts. In a sense, you can think of this as a holistic graph - where Accounts, Reviews, and Products share edges that form a relationship. For example, we might say that a Product has many reviews (one-to-many), but a Review focuses on only one product (one-to-one).
In a traditional GraphQL setup, we'd write resolvers for each of these types - including resolution of nested types.
An example query may look something like:
query {
topProducts(first: 10) {
name
weight
price
reviews {
body
author {
username
name
}
}
}
}
In traditional REST, this may have required 3 calls - one to get the batched list of products, another to get the reviews for each product, another to retrieve our users. A less than ideal REST implementation may take more than 3 calls.
But what about the microservices part?
If you know anything about large graphs, they usually can be broken into connected components - or sub-graphs. That is, the maximal subset of the space cannot be covered by a union of other sub-graphs.
If we look at our schema above, there are 3 obvious sub-graphs - Users, Products, and Reviews. In a sense, it is not the responsibility of the service handling users to know that it has reviews (or products for that matter). There's also a 4th hidden sub-grpah above - Inventory - in the form of the inStock and shippingEstimate fields on the Product. Our service handling our catalog of products is probably best served separately from the API used by our warehouse.
Naturally, we'd want to break this up as independent services - for separation of concerns and independent scaling. In GraphQL, one can do this by using Federation. Federation allows an API Gateway - called the Federation Gateway - to query each sub-graph for it's part of the schema and "stitch" them back into the larger graph. When this gateway receives a query, it can determine which part of the query can be serviced by each backing sub-graph and then executes the queries needed against each service. On some implementations - these executions can occur in parallel.
Broken up, this may look like:
User Service:
extend type Query {
me: User
}
type User @key(fields: "id") {
id: ID!
name: String
username: String
}
Product Service:
extend type Query {
topProducts(first: Int = 5): [Product]
}
type Product @key(fields: "upc") {
upc: String!
name: String
price: Int
weight: Int
}
Warehouse Service:
extend type Product @key(fields: "upc") {
upc: String! @external
weight: Int @external
price: Int @external
inStock: Boolean
shippingEstimate: Int @requires(fields: "price weight")
}
Review Service:
type Review @key(fields: "id") {
id: ID!
body: String
author: User @provides(fields: "username")
product: Product
}
extend type User @key(fields: "id") {
id: ID! @external
username: String @external
reviews: [Review]
}
extend type Product @key(fields: "upc") {
upc: String! @external
reviews: [Review]
}
I'll note, when a service expects fields (to be able to do a resolution) it will either declare them as @external (e.g. coming from another service) or @requires. @requires is specifically in the fact the client may not specify the required fields (e.g. price, weight), but they are needed by the service to produce an output (e.g. shippingEstimate).
Ok, but why isn't this rubbish?
In my mind, GraphQL makes it very easy to think in terms of "Domain" models and boundaries. A sub-graph is - in essence - a domain boundary. The additional strong typing and possible parallel execution fits right in line with my own methodologies and technological attractions (Rust)
Top comments (0)