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:

account/service.go

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:

account/up.sql

The above data definition SQL file will be copied and executed inside the Docker container.

account/db.dockerfile

PostgreSQL database is accessed through a repository interface.

account/repository.go

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:

account/account.proto

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.

account/server.go

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:

catalog/service.go

Database

The repository implements abstractions over Elasticsearch by using olivere/elastic package underneath.

catalog/repository.go

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.

catalog/catalog.proto

Searching and retrieving by IDs is missing, while GetProductsRequest message contains extra fields.

This is how the GetProducts functions looks like:

catalog/server.go

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.

order/service.go

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.

order/up.sql

The Repository interface is very simple.

order/repository.go

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:

order/order.proto

Running the server requires passing in the necessary URLs for other services.

order/server.go

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.

graphql/graph/models.go

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.

graphql/graph/graph.go

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.