67 Connecting Multiple GraphQL Services in Go
67 Connecting Multiple GraphQL Services in Go
As a microservices system grows in scale, the need to integrate several services usually arises so that you can deliver a consistent API experience. One increasingly popular approach is GraphQL federation, where several services, each with their own GraphQL API, are combined into a single unified gateway. In this article, I’ll walk through how to connect multiple GraphQL services in Go, complete with code examples, request simulations, and flow diagrams for building a simple federation.
Why GraphQL Federation?
Before diving into the implementation, let’s first understand the why. Here are a few reasons behind the popularity of GraphQL federation:
| Benefit | Explanation |
|---|---|
| API Consistency | Consumers only need to connect to a single GraphQL endpoint. |
| Independent Development | Each team can build and release its GraphQL schema independently. |
| Scalability | Services can be split apart and scaled on their own. |
| Single Source of Truth | The gateway can unify data from various sources into one large graph. |
The GraphQL Library Landscape in Go
At the time of writing, the most popular GraphQL libraries in Go include:
graphql-gogqlgen— more feature-rich and widely adopted in production.graph-gophers/graphql-go
When it comes to federation, the Go ecosystem isn’t yet as mature as Node.js. Even so, with a few tricks and a lightweight orchestrator, a federation solution can still be built. The approach we’ll use here is the Gateway BFF Pattern.
Case Study: Combining user-service and post-service
Imagine we have two services:
- user-service: serves user data.
- post-service: serves post data, with a reference to a user.
We want the gateway to expose a single GraphQL endpoint that can access the combined data, for example: retrieving a list of posts along with their authors’ names.
1. Architecture Structure
flowchart TD
subgraph Backend
US(User Service)
PS(Post Service)
end
GQ[GraphQL Gateway] --> US
GQ --> PS
CLIENTS((Clients)) --> GQ
1. Creating the Microservice Schemas
Let’s start by defining the schema for each service.
user-service/schema.graphqls
1type User {
2 id: ID!
3 name: String!
4 email: String!
5}
6
7type Query {
8 user(id: ID!): User
9 users: [User!]!
10}post-service/schema.graphqls
1type Post {
2 id: ID!
3 title: String!
4 content: String!
5 authorID: ID!
6}
7
8type Query {
9 post(id: ID!): Post
10 posts: [Post!]!
11}2. Building the GraphQL Service in Go (gqlgen)
Here’s a simple implementation example for user-service (only the essential file, kept concise for clarity):
user-service/main.go
1package main
2
3import (
4 "log"
5 "net/http"
6 "github.com/99designs/gqlgen/graphql/handler"
7 "github.com/99designs/gqlgen/graphql/playground"
8 "user-service/graph"
9 "user-service/graph/generated"
10)
11
12func main() {
13 srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))
14
15 http.Handle("/", playground.Handler("GraphQL playground", "/query"))
16 http.Handle("/query", srv)
17
18 log.Println("connect to http://localhost:8081/ for user service playground")
19 log.Fatal(http.ListenAndServe(":8081", nil))
20}The resolver implementation can use simple in-memory data, and the same applies to post-service.
3. Building the GraphQL Gateway (API Orchestrator)
This is the heart of the article: connecting two (or more) GraphQL services into a single orchestrator. In Go, we don’t yet have full Apollo Federation support, so we’ll use Schema Stitching in a BFF style.
Gateway Schema
We want users to be able to run a query like the following:
1{
2 posts {
3 id
4 title
5 author {
6 id
7 name
8 }
9 }
10}This means we need to add a “author” virtual field to the Post graph in the gateway, whose resolver makes a remote call to user-service.
Gateway Code Example
Installing the supporting packages:
1go get github.com/99designs/gqlgen
2go get github.com/machinebox/graphqlgateway/schema.graphqls
1type User {
2 id: ID!
3 name: String!
4 email: String!
5}
6
7type Post {
8 id: ID!
9 title: String!
10 content: String!
11 author: User!
12}
13
14type Query {
15 posts: [Post!]!
16 post(id: ID!): Post
17}gateway/resolver.go (the core of the stitch pattern)
1package graph
2
3import (
4 "context"
5 "github.com/machinebox/graphql"
6)
7
8type Resolver struct{}
9
10type Post struct {
11 ID string
12 Title string
13 Content string
14 AuthorID string
15}
16
17func (r *queryResolver) Posts(ctx context.Context) ([]*Post, error) {
18 // call post-service
19 client := graphql.NewClient("http://localhost:8082/query")
20 req := graphql.NewRequest(`
21 query { posts { id title content authorID } }
22 `)
23 var resp struct {
24 Posts []*Post
25 }
26 if err := client.Run(ctx, req, &resp); err != nil {
27 return nil, err
28 }
29 return resp.Posts, nil
30}
31
32func (r *postResolver) Author(ctx context.Context, obj *Post) (*User, error) {
33 // call user-service
34 client := graphql.NewClient("http://localhost:8081/query")
35 req := graphql.NewRequest(`
36 query ($id: ID!) { user(id: $id) { id name email } }
37 `)
38 req.Var("id", obj.AuthorID)
39 var resp struct {
40 User *User
41 }
42 if err := client.Run(ctx, req, &resp); err != nil {
43 return nil, err
44 }
45 return resp.User, nil
46}Explanation:
- The
Postsquery fetches data from post-service remotely. - For each
author, the resolver makes a remote GraphQL call to user-service.
4. Request Simulation
1query {
2 posts {
3 id
4 title
5 author {
6 id
7 name
8 }
9 }
10}Flow Diagram
sequenceDiagram
participant CLIENT
participant GATEWAY
participant POST_SERVICE
participant USER_SERVICE
CLIENT->>GATEWAY: Query posts { id title author { id name } }
GATEWAY->>POST_SERVICE: Query posts { id title content authorID }
POST_SERVICE-->>GATEWAY: Posts Data
loop For each Post
GATEWAY->>USER_SERVICE: Query user { id name email } (with authorID)
USER_SERVICE-->>GATEWAY: User Data
end
GATEWAY-->>CLIENT: List of posts & authors
5. Implications and Limitations
| Aspect | Explanation |
|---|---|
| Performance | For every post, there’s a call to user-service. Limit N+1, ideally by using batching (DataLoader). |
| Error Handling | Errors in any one service must be handled properly in the orchestrator. |
| Schema Management | Schema changes in each service must always be synced with the gateway. |
| Auth & Resilience | The gateway becomes the central point for security and throttling. |
6. Towards Production
The steps above represent a minimal viable architecture. For production:
- Use DataLoader to avoid N+1 remote calls.
- Pay attention to timeouts and circuit breakers.
- Implement caching for specific needs.
- Monitor aggregate latency.
- For full federation (Apollo Federation style), keep an eye on gqlgen feature request 980 .
Conclusion
With a bit of schema stitching, we can connect multiple GraphQL services in Go without a heavy federation framework. This BFF orchestrator pattern is a great fit for small to mid-size teams that want the convenience and consistency of a global API.
As your needs grow more complex (hundreds of subgraphs, highly dynamic schemas), it’s better to keep track of native GraphQL federation developments in Go or, if feasible, mix in Node.js for the federation gateway.
Happy experimenting, and may your code be better orchestrated! 🚀
References