Schema Stitching & Federation: Not the Best Solutions for GraphQL Microservices
Let's start by saying that Apollo did a fantastic job in bringing GraphQL where it is today. Libraries like Apollo Client and Apollo Server get a couple of million downloads per week. They also created schema stitching, which is now maintained in the open-source community, to bring together (micro)services that support GraphQL. Later they released Federation as a more complete solution.
But this tool has one obvious design mistake. It is only usable by microservices that support Federation, meaning you cannot use it for every service. With StepZen, we're doing this differently. Instead of designing a specification for your microservices, or any other data source, we've created an SDL-first approach to combining GraphQL schemas. In this post, you'll learn about different methods to combine GraphQL microservices. And how StepZen compares to, for example, schema stitching and Apollo Federation.
Using Subschemas to Stitch GraphQL Services
There are multiple ways to combine microservices with GraphQL. One of those methods is schema stitching, which was the first common approach to combining multiple GraphQL schemas. Schema stitching lets you do this by extending the schema with different "sub" schemas. If you want to use schema stitching to combine multiple microservices, you must create a separate GraphQL server instance. This server (or gateway) will contain the main schema comprised of the subschemas for every microservice. The values in these subschemas come from additional resolvers that you need to write for your GraphQL server. Schema stitching is part of GraphQL Tools, of which you can see an example below:
import { stitchSchemas } from '@graphql-tools/stitch';
import { delegateToSchema } from '@graphql-tools/delegate';
// Build the combined schema and set up the extended schema and resolver
const schema = stitchSchemas({
subschemas: [localSubschema, productsSubschema, cmsSubschema],
typeDefs: `
extend type Product {
cmsMetaData: [Cms_Product]!
}
`,
resolvers: {
Product: {
cmsMetaData: {
selectionSet: `{ id }`,
resolve(product, args, context, info) {
// Get the data for the extended type from the subschema for the CMS
return delegateToSchema({
schema: cmsSubschema,
operation: 'query',
fieldName: 'cms_allProducts',
args: { filter: { id: product.id } },
context,
info,
});
},
},
},
},
});
Above, you can see an example of schema stitching. The function stitchSchemas
from @graphql-tools/stitch
creates one schema from multiple subschemas. The resolver
field creates an extended type that combines data from the subschemas productsSubschema
and cmsSubschema
. To combine this data the function delegateToSchema
from @graphql-tools/delegate
is used. You can use the returned output of stitchSchemas
as an executable schema for most JavaScript libraries to create a GraphQL Server. For a complete implementation on schema stitching, have a look at this talk I did at GraphQL Conf 2021.
With schema stitching, you don't need to change these underlying services to combine them using GraphQL. But writing separate resolvers for the server that combines every subschema is time-consuming, hard to maintain, and even harder to scale. Also, you do have to build, deploy and maintain this GraphQL server that brings all your services together.
Federate Subgraphs With Apollo Federation
Federation was introduced as a solution to the problems mentioned in the previous section. With Federation, you don't have to write resolvers for every subschema in a gateway server. Instead, you use a subgraph with custom directives to combine (micro)services. In theory, you could connect any schema that uses GraphQL as a subschema, but this isn't the case in practice. Federation uses custom directives to combine the subschemes into one schema. To support these custom directives, you need to edit the schema and resolvers for every microservice to adhere to the Apollo Federation Specification. The amount of code you need to write is not as extensive as with schema stitching. Still, it introduces a barrier.
Every one of your microservices must support Apollo Federation, before you can use it as a subgraph. This includes:
-
Editing the schema of your microservices to include Federation custom directives like
@key
,@external
or@provides
: -
Extending the
Query
type in the GraphQL schema for your microservices to include the queries_entitites
and_service
: -
Adding the queries that you've added to the extended schema to the resolvers of the microservices you want to add as subgraphs.
If you're using a library that supports Federation out-of-the-box, you can skip one or more of the steps above.
After you've added Federation support to all your microservices, you can combine these microservices in a gateway. This gateway server combines the microservices into one schema:
import { ApolloServer } from 'apollo-server';
import { ApolloGateway } from '@apollo/gateway';
const gateway = new ApolloGateway({
serviceList: [
{ name: 'auth', url: 'http://localhost:3001/graphql' },
{ name: 'products', url: 'http://localhost:3002/graphql' },
{ name: 'users', url: 'http://localhost:3003/graphql' }
],
});
(async () => {
const server = new ApolloServer({
gateway
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
})();
The field serviceList
in ApolloGateway
contains all the microservices that expose a GraphQL schema compliant to the Apollo Federation Specification. This schema can be used as a subgraph for the gateway and gives you a standardized implementation for services designed according to the specs. Teams within your organization can make changes to their microservices without having to make changes to the gateway or to any of the other microservices. This approach cannot be used for services that don't support Federation and cannot be edited or maintained by a different organization. Besides this, you're still responsible for building, deploying, and maintaining the gateway that you create with Apollo Federation.
Combining Microservices SDL-first Using StepZen
Both methods are suitable for combining the GraphQL schemas of microservices, but this comes at a cost. To combine your microservices with schema stitching or Federation, you need to make changes to resolvers. And possibly even changes to the GraphQL schemas of your microservices. Also, you need to write code to build a GraphQL gateway server that combines the subschemas or subgraphs of your underlying services. Not to mention taking responsibility for deployments and maintenance of this server - including its performance and scalability.
With StepZen, we've taken a more declarative and developer-friendly approach. You can combine microservices by using GraphQL SDL to create a data layer with configuration only. No need to write resolver code or to modify the schemas of your microservices. As opposed to Federation you can use StepZen with every microservice. You don't have to transform a service for Federation support and can use it to combine other backends like REST and database. Instead of resolving the data from every GraphQL schema on the microservice, it is done from one centralized data layer. The diagram below shows how the two compare when you create a gateway or data layer to combine different GraphQL services.
Combining multiple microservices with StepZen is done through GraphQL SDL. In one or multiple GraphQL schemas, you can define which microservices or backends must be combined and link them together using custom directives. These custom directives specify the origin of the data and hold the details configure them.
Suppose you want to combine three microservices that use both GraphQL and REST. For the GraphQL services, you need to define the location of the GraphQL schemas to combine them, after which you can include their types in the main GraphQL schema. For the REST service, the approach is almost similar:
type Auth {
id: Int!
access_token: String!
}
type Post {
id: Int!
title: String!
imageUrl: String!
author: Int
}
type User {
id: Int!
name: String!
posts: [Post] @materializer(query: "posts")
}
type Query {
token: Auth
@rest(
endpoint: "https://localhost:3001/api/token?grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret"
configuration: "auth_service"
)
posts: [Post]
@rest(endpoint: "http://localhost:3002/api/posts")
users: [User]
@graphql(endpoint: "http://localhost:3003/graphql")
}
The auth
query even uses configuration from a YAML file to get the authentication details. The types User
is linked to the Posts
type on the field posts
using the custom directive called @materializer
. This directive is for making simple combinations between types coming from data of different underlying microservices.
Using only GraphQL SDL, you now have combined these three microservices. But you can use the same approach for monolith APIs built for REST, GraphQL, or traditional databases.
StepZen Data Layer For Microservices: An Example
For a complete example of using StepZen to combine microservices, look at this example repository. It shows more complex relations between types and microservices using, for example, the @sequence
directive.
In contrast to schema stitching and Federation, StepZen eliminates the need to write resolver code or edit your underlying microservices. But it doesn't stop there. You can deploy any GraphQL schema as a fully performant GraphQL server to StepZen. It takes the custom directives in your schema and transforms them into a data layer deployed in the cloud. No longer do you need to worry about how your gateway or data layer performs or scales, as no infrastructure needs to be managed by your team.
Want to learn more about StepZen? Try it out here or ask any question on the Discord here.