The other day, I watched a single developer team try to scale a GraphQL API. What began as a neat, unified schema slowly became a tangled web of types and resolvers. New features introduced breaking changes for everyone. Deployments were risky and slow. It was a clear sign: a single, monolithic GraphQL server often hits a wall as an application grows. This experience is what drove me to explore a different path—one that keeps the power of GraphQL but distributes the responsibility.
How do you scale your API without creating a deployment bottleneck?
The answer lies in composition. Instead of one massive server, you split your domain into separate, focused services. Each service owns its data and exposes its own GraphQL schema. A central gateway then stitches these individual schemas into one cohesive supergraph that your clients can query. This is GraphQL Federation. It lets teams work independently while presenting a single, consistent API.
Let’s get our hands dirty. First, we’ll set up a project with multiple services. Imagine an e-commerce platform. We’ll have separate services for users, products, and orders.
Here’s a simple user service subgraph schema. Notice the @key directive—this tells the gateway that User is a shared entity that other services can reference.
// user-service/schema.ts
import { gql } from 'graphql-tag';
export const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
}
type Query {
user(id: ID!): User
}
`;
The product service can now extend that User type, adding fields that only it can provide. This is the magic of federation: services enrich shared types.
// product-service/schema.ts
import { gql } from 'graphql-tag';
export const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@extends", "@external"])
type User @key(fields: "id") @extends {
id: ID! @external
reviews: [ProductReview!]! # This service adds this field
}
type ProductReview {
id: ID!
productId: ID!
rating: Int!
comment: String
}
`;
But how does the gateway know where to fetch the User data from? Each service must provide a reference resolver for the entities it owns. When the gateway gets a query for a user’s reviews, it first gets the user’s ID from the user service, then calls the product service’s reference resolver to fetch the reviews for that specific ID.
// product-service/resolvers.ts
const resolvers = {
User: {
reviews: (userReference) => {
// userReference will be { id: 'user-123' }
return fetchReviewsByUserId(userReference.id);
}
}
};
Setting up the gateway is straightforward. It polls your services for their schemas and composes them.
// gateway/index.ts
import { ApolloGateway } from '@apollo/gateway';
import { ApolloServer } from '@apollo/server';
const gateway = new ApolloGateway({
serviceList: [
{ name: 'users', url: 'http://localhost:4001/graphql' },
{ name: 'products', url: 'http://localhost:4002/graphql' },
],
pollIntervalInMs: 30000, // Periodically check for schema updates
});
const server = new ApolloServer({ gateway });
// Start your server
This architecture introduces new challenges, though. What happens when one service is slow? Or when you need to verify a user’s permissions across multiple services? For performance, implement DataLoader patterns within each subgraph to batch and cache database calls. For authentication, a common practice is to have the gateway validate a JWT and attach user claims to the request context, which is then passed to every subgraph.
Thinking about deployment, you might wonder how to manage schema changes safely. Apollo Studio offers tools for schema checks and progressive rollout, which are vital for a federated system. You can validate that a new subgraph schema is compatible with the supergraph before it ever reaches production.
So, is federation the right choice for every project? Not exactly. It adds operational complexity. For a small team or a simple application, a well-organized monolithic GraphQL server is often perfect. But when you feel the pain of coordinating changes across a large codebase, or need teams to deploy features independently, federation provides a structured way to scale.
I’ve found this approach transforms how teams collaborate. Developers gain autonomy, releases become faster and less frightening, and the system can grow organically. If you’ve struggled with a bloated central API, I encourage you to try building a federated gateway.
What scaling challenges are you facing in your current architecture? Share your thoughts in the comments below. If this breakdown was helpful, please like and share it with your network. Let’s keep the conversation going