A few months ago, a really nice Go package vektah/gqlgen for GraphQL became popular. This article describes how GraphQL is implemented in "Spidey", an exemplary microservices based online store.
Some parts listed below are missing, but complete source code is available on GitHub.
Architecture
Spidey encompasses three services that are exposed to the user through a GraphQL gateway. Communication within the cluster is through gRPC.
Account service manages accounts and catalog manages products. Order service deals with creating orders. It talks to the other two services to properly validate orders.
Individual services have three layers: server, service and repository. Server is responsible for communication, in Spidey's case its gRPC. Service contains any business logic. Repository is responsible for writing and reading data from a database.
Getting started
Running Spidey requires Docker, Docker Compose, Go, Protocol Buffers compiler and its Go plugin, and the super cool vektah/gqlgen package.
You'll also need vgo tool, which is in early stages of development. The dep tool will work as well, but included go.mod file will be ignored.
Docker setup
Each service is implemented in its own subdirectory and contains at least app.dockerfile file. The db.dockerfile is used to build the database image.
All services are defined inside docker-compose.yaml file.
Here's an extract for the account service:
The context is specifically set to ensure that vendor directory can be copied inside the Docker container. All services share the same dependencies and some need each other's definitions.
Account service
Account service exposes functions for creating and retrieving accounts.
Service
API of the account service is defined with the following interface:
Implementation needs a reference to the repository.
The service is responsible for any business logic. The PostAccount functions is implemented like this:
It leaves parsing wire format to the server and dealing with a database to the repository.
Database
Data model for an account is very simple:
The above data definition SQL file will be copied and executed inside the Docker container.
PostgreSQL database is accessed through a repository interface.
Repository is implemented as a wrapper over the Go's standard library SQL package.
gRPC
The gRPC service is defined with the following Protocol Buffers file:
Because the package is set to pb, the generated code will be available from the pb subpackage.
gRPC code is generated with the command specified at the top of account/server.go file and by using Go's generate command.
Running the following command will generate code inside pb subdirectory.
Server works as an adapter over the Service interface, and transforming request and response types accordingly.
Here's how the PostAccount functions looks like:
Usage
gRPC server is initialized inside account/cmd/account/main.go file.
A client struct is implemented inside account/client.go file for convenience. With it, account service can be used without worrying about the underlying RPC implementation, as seen later on.
Catalog service
Catalog service deals with products in the Spidey store. It's implemented similarly as the account service, but uses Elasticsearch to persist products.
Service
Account service conforms to the following interface:
Database
The repository implements abstractions over Elasticsearch by using olivere/elastic package underneath.
Because Elasticsearch stores IDs separately from documents, there's a helper struct for products which doesn't contain the ID.
Inserting products into the database involves copying over all the fields into the productDocument struct:
gRPC
The gRPC service is defined in the catalog/catalog.proto file, and implemented in the catalog/server.go file.
One notable difference from the account service is that it doesn't define all the endpoints from the service interface.
Searching and retrieving by IDs is missing, while GetProductsRequest message contains extra fields.
This is how the GetProducts functions looks like:
It decides what service function to call based on arguments given. The goal is to mimic how a REST HTTP endpoint would look like.
Having one endpoint, which looks like /products?[ids=...]&[query=...]&skip=0&take=100, is easier to work with while consuming the API.
Order service
Order service is a bit trickier. It needs to call account and catalog services to validate requests, since an order can only be created for an account and products that exist.
Service
The Service interface defines functions for creating orders and retrieving all orders made by some account.
Database
An order can contain multiple products, so the data model must support that. The order_products table below describes an ordered product with an ID of product_id and the quantity of such products. The product_id field will have to be retrieved from the catalog service.
The Repository interface is very simple.
But the implementation is not quite so simple.
One order must be inserted in two steps using a transaction, and then selected using a join statement.
Reading an order from a database requires parsing tabular data into the object hierarchy. The code below works by traversing through returned rows and groups products into orders based on order's ID.
gRPC
The gRPC server needs to contact account and catalog services before delegating the request to the order service implementation.
The protocol is defined as follows:
Running the server requires passing in the necessary URLs for other services.
Creating an order involves calling the account service, to check if the account exists, and then doing the same for products. Fetching products is also required for calculating the total price, which is handled by the service. You don't want users passing in their own sum.
When querying for orders made by specific account, calling the catalog service is also necessary because product details (name, price and description) are needed.
GraphQL service
The GraphQL schema is defined inside graphql/schema.graphql file.
The gqlgen tool will generate a bunch of types, but more control is needed for the Order model. It is specified in the graphql/types.json file,
so the model wont be automatically generated.
The Order struct can now be implemented manually.
The command for generating types is defined at the top of graphql/graph/graph.go file.
It can be run with:
GraphQL server has references to all other services.
The GraphQLServer struct needs to implement all generated resolvers. Mutations can be found in graphql/graph/mutations.go and queries in graphql/graph/queries.go.
Mutations call the relevant service by using its client, and passing in the arguments.
Queries can have other nested queries. In Spidey's example, querying for an account can also query its orders, as seen in the Account_orders function.
Wrapping up
To run Spidey, execute the following commands:
And open http://localhost:8000/playground in your browser.
In the GraphQL tool presented, try creating an account:
Which returns:
Then create some products:
Note the returned IDs:
Then order something:
And verify the returned cost:
The entire source code is available on GitHub.