Thanks! We'll be in touch in the next 12 hours
Oops! Something went wrong while submitting the form.

Implementing Federated GraphQL Microservices using Apollo Federation

Introduction

GraphQL has revolutionized how a client queries a server. With the thin layer of GraphQL middleware, the client has the ability to query the data more comprehensively than what’s provided by the usual REST APIs.

One of the key principles of GraphQL involves having a single data graph of the implementing services that will allow the client to have a unified interface to access more data and services through a single query. Having said that, it can be challenging to follow this principle for an enterprise-level application on a single, monolith GraphQL server.

The Need for Federated Services

James Baxley III, the Engineering Manager at Apollo, in his talk here, puts forward the rationale behind choosing an independently managed federated set of services very well.

To summarize his point, let’s consider a very complex enterprise product. This product would essentially have multiple teams responsible for maintaining different modules of the product. Now, if we’re considering implementing a GraphQL layer at the backend, it would only make sense to follow the one graph principle of GraphQL: this says that to maximize the value of GraphQL, we should have a single unified data graph that’s operating at the data layer of this product. With that, it will be easier for a client to query a single graph and get all the data without having to query different graphs for different data portions.

However, it would be challenging to have all of the huge enterprise data graphs’ layer logic residing on a single codebase. In addition, we want teams to be able to independently implement, maintain, and ship different schemas of the data graph on their own release cycles.

Though there is only one graph, the implementation of that graph should be federated across multiple teams.

Now, let’s consider a massive enterprise e-commerce platform as an example. The different schemas of the e-commerce platform look something like:

E-commerce platform set of schemas
Fig:- E-commerce platform set of schemas

Considering the above example, it would be a chaotic task to maintain the graph implementation logic of all these schemas on a single code base. Another overhead that this would bring is having to scale a huge monolith that’s implementing all these services. 

Thus, one solution is a federation of services for a single distributed data graph. Each service can be implemented independently by individual teams while maintaining their own release cycles and having their own iterations of their services. Also, a federated set of services would still follow the Onegraph principle of GraphQL, which will allow the client to query a single endpoint for fetching any part of the data graph.

To further demonstrate the example above, let’s say the client asks for the top-five products, their reviews, and the vendor selling them. In a usual monolith GraphQL server, this query would involve writing a resolver that’s a mesh of the data sources of these individual schemas. It would be a task for teams to collaborate and come up with their individual implementations. Let’s consider a federated approach with separate services implementing products, reviews, and vendors. Each service is responsible for resolving only the part of the data graph that includes the schema and data source. This makes it extremely streamlined to allow different teams managing different schemas to collaborate easily.

Another advantage would be handling the scaling of individual services rather than maintaining a compute-heavy monolith for a huge data graph. For example, the products service is used the most on the platform, and the vendors service is scarcely used. In case of a monolith approach, the scaling would’ve had to take place on the overall server. This is eliminated with federated services where we can independently maintain and scale individual services like the products service.

Federated Implementation of GraphQL Services

A monolith GraphQL server that implements a lot of services for different schemas can be challenging to scale. Instead of implementing the complete data graph on a single codebase, the responsibilities of different parts of the data graph can be split across multiple composable services. Each one will contain the implementation of only the part of the data graph it is responsible for. Apollo Federation allows this division of services and follows a declarative programming model to allow splitting of concerns.

Architecture Overview

This article will not cover the basics of GraphQL, such as writing resolvers and schemas. If you’re not acquainted with the basics of GraphQL and setting up a basic GraphQL server using Apollo, I would highly recommend reading about it here. Then, you can come back here to understand the implementation of federated services using Apollo Federation.

Apollo Federation has two principal parts to it:

  • A collection of services that distinctly define separate GraphQL schemas
  • A gateway that builds the federated data graph and acts as a forefront to distinctly implement queries for different services
Apollo Federation Architecture
Fig:- Apollo Federation Architecture

Separation of Concerns

The usual way of going about implementing federated services would be by splitting an existing monolith based on the existing schemas defined. Although this way seems like a clear approach, it will quickly cause problems when multiple Schemas are involved.

To illustrate, this is a typical way to split services from a monolith based on the existing defined Schemas:


Image for post

In the example above, although the tweets field belongs to the User schema, it wouldn’t make sense to populate this field in the User service. The tweets field of a User should be declared and resolved in the Tweet service itself. Similarly, it wouldn’t be right to resolve the creator field inside the Tweet service.

The reason behind this approach is the separation of concerns. The User service might not even have access to the Tweet datastore to be able to resolve the tweets field of a user. On the other hand, the Tweet service might not have access to the User datastore to resolve the creator field of the Tweet schema.

Image for post

Considering the above schemas, each service is responsible for resolving the respective field of each Schema it is responsible for.

Implementation

To illustrate an Apollo Federation, we’ll be considering a Nodejs server built with Typescript. The packages used are provided by the Apollo libraries.

CODE: https://gist.github.com/velotiotech/c2115dcfeeb19b34e2957e049b9ffa29.js 

Some additional libraries to help run the services in parallel:

CODE: https://gist.github.com/velotiotech/fac5f711387b03439e3369869ff69ea4.js

Let’s go ahead and write the structure for the gateway service first. Let’s create a file gateway.ts:

CODE: https://gist.github.com/velotiotech/dfe073237bb0b6e809b914c651867932.js 

And add the following code snippet:

CODE: https://gist.github.com/velotiotech/9d5286d9913a66299e0c668cbeeb1923.js

Note the serviceList is an empty array for now since we’ve yet to implement the individual services. In addition, we pass the subscriptions: false option to the apollo server config because currently, Apollo Federation does not support subscriptions.

Next, let’s add the User service in a separate file user.ts using:

CODE: https://gist.github.com/velotiotech/ddc822b345f6e914543933540610330d.js

The code will go in the user service as follows:

CODE: https://gist.github.com/velotiotech/ba76742648e6e983fa81f59091b1b2d5.js

Let’s break down the code that went into the User service.

Consider the User schema definition:

CODE: https://gist.github.com/velotiotech/347dcdc39233e2429aa3f5c94a3d154c.js

The @key directive helps other services understand the User schema is, in fact, an entity that can be extended within other individual services. The fields will help other services uniquely identify individual instances of the User schema based on the id.

The Query and the Mutation types need to be extended by all implementing services according to the Apollo Federation documentation since they are always defined on a gateway level.

As a side note, the User model imported from datasources/model/User

import User from './datasources/models/User'; is essentially a Mongoose ORM Model for MongoDB that will help in all the CRUD operations of a User entity in a MongoDB database. In addition, the mongoStore() function is responsible for establishing a connection to the MongoDB database server.

The User model implementation internally in Mongoose ORM looks something like this:

CODE: https://gist.github.com/velotiotech/7eacf97f20a26859ccfc3c6272458362.js

In the Query type, the users and the user(id: ID!) queries fetch a list or the details of individual users.

In the resolvers, we define a __resolveReference function responsible for returning an instance of the User entity to all other implementing services, which just have a reference id of a User entity and need to return an instance of the User entity. The ref parameter is an object { id: ‘userEntityId’ } that contains the id of an instance of the User entity that may be passed down from other implementing services that need to resolve the reference of a User entity based on the reference id. Internally, we fire a mongoose .findOne query to return an instance of the User from the users database based on the reference id. To illustrate the resolver, 

CODE: https://gist.github.com/velotiotech/936607a47b933da329cd5017f02c5149.js

At the end of the file, we make sure the service is running on a unique port number 4001, which we pass as an option while running the apollo server. That concludes the User service.

Next, let’s add the tweet service by creating a file tweet.ts using:

CODE: https://gist.github.com/velotiotech/103bb3ddf437c2c543ae24f112f33ce7.js 

The following code goes as a part of the tweet service:

CODE: https://gist.github.com/velotiotech/c21304e38df6e69b84b6c725e37b80f9.js

Let’s break down the Tweet service as well

CODE: https://gist.github.com/velotiotech/85733358a8768e922507002abbe43a97.js

The Tweet schema has the text field, which is the content of the tweet, a unique id of the tweet,  and a creator field, which is of the User entity type and resolves into the details of the user that created the tweet:

CODE: https://gist.github.com/velotiotech/8b838283cb93db03afb8870715237d37.js

We extend the User entity schema in this service, which has the id field with an @external directive. This helps the Tweet service understand that based on the given id field of the User entity schema, the instance of the User entity needs to be derived from another service (user service in this case).

As we discussed previously, the tweets field of the extended User schema for the user entity should be resolved in the Tweet service since all the resolvers and access to the data sources with respect to the Tweets entity resides in this service.

The Query and Mutation types of the Tweet service are pretty straightforward; we have a tweets and a tweet(id: ID!) queries to resolve a list or resolve an individual instance of the Tweet entity.

Let’s further break down the resolvers:

CODE: https://gist.github.com/velotiotech/76e882a025b7d370288c0a199f311292.js

To resolve the creator field of the Tweet entity, the Tweet service needs to tell the gateway that this field will be resolved by the User service. Hence, we pass the id of the User and a __typename for the gateway to be able to call the right service to resolve the User entity instance. In the User service earlier, we wrote a  __resolveReference resolver, which will resolve the reference of a User based on an id.

CODE: https://gist.github.com/velotiotech/f0e15567d14938bca55ec5ed182b7b9a.js

Now, we need to resolve the tweets field of the User entity extended in the Tweet service. We need to write a resolver where we get the parent user entity reference in the first argument of the resolver using which we can fire a Mongoose ORM query to return all the tweets created by the user given its id.

At the end of the file, similar to the User service, we make sure the Tweet service runs on a different port by adding the port: 4002 option to the Apollo server config. That concludes both our implementing services.

Now that we have our services ready, let’s update our gateway.ts file to reflect the added services:

CODE: https://gist.github.com/velotiotech/acc419e97b302b71dc33ba34bf3a6b88.js

We’ve added two services to the serviceList with a unique name to identify each service followed by the URL they are running on.

Next, let’s make some small changes to the package.json file to make sure the services and the gateway run in parallel:

CODE: https://gist.github.com/velotiotech/87fc05ca9abbf947cbf9fa61df0361dc.js

The concurrently library helps run 3 separate scripts in parallel. The server:* scripts spin up a dev server using nodemon to watch and reload the server for changes and ts-node to execute Typescript node.

Let’s spin up our server:

CODE: https://gist.github.com/velotiotech/af2c0a71e3cb985fb1a1a861f1066aa5.js

On visiting the http://localhost:4000, you should see the GraphQL query playground running an Apollo server:

Image for post

Querying and Mutation from the Client

Initially, let’s fire some mutations to create two users and some tweets by those users.

Mutations

Here we have created a user with the username “@elonmusk” that returns the id of the user. Fire the following mutations in the GraphQL playground:

Image for post


We will create another user named “@billgates” and take a note of the ID.

Image for post

Here is a simple mutation to create a tweet by the user “@elonmusk”. Now that we have two created users, let’s fire some mutations to create tweets by those users:

Image for post

Here is another mutation that creates a tweet by the user“@billgates”.

Image for post

After adding a couple of those, we are good to fire our queries, which will allow the gateway to compose the data by resolving fields through different services.

Queries

Initially, let’s list all the tweets along with their creator, which is of type User. The query will look something like:

CODE: https://gist.github.com/velotiotech/b567c9691f4066eee0d6e08ca874982a.js

When the gateway encounters a query asking for tweet data, it forwards that query to the Tweet service since the Tweet service that extends the Query type has a tweet query defined in it. 

On encountering the creator field of the tweet schema, which is of the type User, the creator resolver within the Tweet service is invoked. This is essentially just passing a __typename and an id, which tells the gateway to resolve this reference from another service.

In the User service, we have a __resolveReference function, which returns the complete instance of a user given it’s id passed from the Tweet service. It also helps all other implementing services that need the reference of a User entity resolved.

On firing the query, the response should look something like:

CODE: https://gist.github.com/velotiotech/3be39caca105a7cb3d87c63ac6eb1330.js

Now, let’s try it the other way round. Let’s list all users and add the field tweets that will be an array of all the tweets created by that user. The query should look something like:

CODE: https://gist.github.com/velotiotech/f5df7dd32b85043f0a1e032e92513451.js 

When the gateway encounters the query of type users, it passes down that query to the user service. The User service is responsible for resolving the username field of the query.

On encountering the tweets field of the users query, the gateway checks if any other implementing service has extended the User entity and has a resolver written within the service to resolve any additional fields of the type User.

The Tweet service has extended the type User and has a resolver for the User type to resolve the tweets field, which will fetch all the tweets created by the user given the id of the user.

On firing the query, the response should be something like:

CODE: https://gist.github.com/velotiotech/fdd85383f52757d045b790d5ecb9969a.js 

Conclusion

To scale an enterprise data graph on a monolith GraphQL service brings along a lot of challenges. Having the ability to distribute our data graph into implementing services that can be individually maintained or scaled using Apollo Federation helps to quell any concerns.

There are further advantages of federated services. Considering our example above, we could have two different kinds of datastores for the User and the Tweet service. While the User data could reside on a NoSQL database like MongoDB, the Tweet data could be on a SQL database like Postgres or SQL. This would be very easy to implement since each service is only responsible for resolving references only for the type they own.

Final Thoughts

One of the key advantages of having different services that can be maintained individually is the ability to deploy each service separately. In addition, this also enables deployment of different services independently to different platforms such as Firebase, Lambdas, etc.

A single monolith GraphQL server deployed on an instance or a single serverless platform can have some challenges with respect to scaling an instance or handling high concurrency as mentioned above.

By splitting out the services, we could have a separate serverless function for each implementing service that can be maintained or scaled individually and also a separate function on which the gateway can be deployed.

One popular usage of GraphQL Federation can be seen in this Netflix Technology blog, where they’ve explained how they solved a bottleneck with the GraphQL APIs in Netflix Studio . What they did was create a federated GraphQL microservices architecture, along with a Schema store using Apollo Federation. This solution helped them create a unified schema but with distributed ownership and implementation.

Did you like the blog? If yes, we're sure you'll also like to work with the people who write them - our best-in-class engineering team.

We're looking for talented developers who are passionate about new emerging technologies. If that's you, get in touch with us.

Explore current openings