Delegating your GraphQL schema isn’t just about offloading work; it’s about leveraging specialized services to handle specific parts of your data graph, allowing your main gateway to focus on orchestrating and enhancing requests.
Let’s see this in action. Imagine you have a User type that’s managed by a dedicated User Service, and a Product type managed by a Product Service. Your main GraphQL Gateway needs to resolve queries that involve both.
# User Service Schema
type User {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
}
# Product Service Schema
type Product {
id: ID!
name: String!
price: Float!
owner: User # This is a reference to the User type
}
type Query {
product(id: ID!): Product
}
# Gateway Schema (combines both)
type User {
id: ID!
name: String!
email: String!
}
type Product {
id: ID!
name: String!
price: Float!
owner: User
}
type Query {
user(id: ID!): User
product(id: ID!): Product
}
When a client requests query { product(id: "prod-123") { name owner { name } } }, the gateway receives this. It knows product is handled by the Product Service. The Product Service resolves product(id: "prod-123") { name } and crucially, for the owner field, it returns a User object with just the __typename and id (e.g., { __typename: "User", id: "user-456" }). This is the delegation part. The gateway then sees the owner field requires name, which is a field on the User type. It knows the User type is handled by the User Service. It then makes a separate request to the User Service for user(id: "user-456") { name }.
The fundamental problem this solves is the complexity of a monolithic GraphQL schema. As your application grows, managing a single, massive schema becomes unwieldy. Different teams might own different domains (users, products, orders), and a single schema means they’re constantly stepping on each other’s toes. Schema delegation allows these teams to own and evolve their respective schemas independently. The gateway acts as a smart router, composing these independent schemas into a unified API for clients.
Internally, this works by having your gateway service receive the incoming query. It analyzes the query and determines which subgraphs (or federated services) are responsible for which parts of the schema. When a field is requested that belongs to a different subgraph, the gateway doesn’t try to resolve it itself. Instead, it delegates that part of the query to the appropriate subgraph. This delegation can happen at various levels:
- Type Delegation: If a type (like
User) is managed by a specific service, any query asking for fields onUserwill be sent to that service. - Field Delegation: Even within a type, specific fields might be resolved by different services. For example, a
Producttype might havenameandpriceresolved by the Product Service, butreviewsresolved by a separate Review Service. - Reference Resolution: This is key in federation. When one service needs to resolve a field that points to a type managed by another service (like
Product.ownerpointing toUser), the first service returns just enough information to identify the object (typically__typenameandid). The gateway then uses this "entity reference" to fetch the full object from the owning service.
The exact levers you control are in how you configure your gateway. For example, in Apollo Federation, you define subgraphs and their typeDefs and resolvers. The gateway then uses a special _entities query to fetch entities across subgraphs. A typical configuration might look like this:
// Gateway server setup (simplified Apollo Federation example)
const { ApolloServer } = require('apollo-server');
const { buildSubgraphSchema } = require('@apollo/federation');
// User subgraph
const typeDefsUser = require('./user.schema');
const resolversUser = require('./user.resolvers');
const userSchema = buildSubgraphSchema([{ typeDefs: typeDefsUser, resolvers: resolversUser }]);
// Product subgraph
const typeDefsProduct = require('./product.schema');
const resolversProduct = require('./product.resolvers');
const productSchema = buildSubgraphSchema([{ typeDefs: typeDefsProduct, resolvers: resolversProduct }]);
const gateway = new ApolloServer({
gateway: {
serviceList: [
{ name: 'users', url: 'http://localhost:4001/graphql' },
{ name: 'products', url: 'http://localhost:4002/graphql' },
],
},
subscriptions: false,
});
gateway.listen().then(({ url }) => {
console.log(`🚀 Gateway ready at ${url}`);
});
The surprising thing about schema delegation, especially with federation, is how it allows for incremental adoption. You don’t need to rewrite your entire backend to start federating. You can extract a single domain, like your User service, into its own subgraph, and have your existing monolithic GraphQL API act as the gateway. The gateway will then delegate queries for users to this new subgraph while still resolving other requests itself. This phased approach significantly de-risks the transition to a microservice-based GraphQL architecture.
The next concept you’ll typically encounter is handling complex cross-subgraph mutations and ensuring transactional integrity.