A single GraphQL schema works well until several teams need to change it at once. Federation splits the graph into subgraphs that each team owns and deploys, while clients still see one endpoint. This guide covers when to federate, Apollo Federation 2 schemas, the Apollo Router, and how to run it on your own servers.
When several teams share one GraphQL schema, it becomes a bottleneck. Federation lets each team own a subgraph; a router composes them into one unified graph. Start monolithic, federate only when team boundaries demand it, keep subgraphs on a private network behind the router, and run schema checks in CI.
When to federate
Don't start here. Federation adds operational complexity.
Stay monolithic when:
- A single team owns the schema
- Fewer than about 20 engineers touch GraphQL
- There are no clear domain boundaries
Federate when:
- Multiple teams need autonomy over their slice of the graph
- Domains are genuinely separate (users, orders, inventory)
- Monolith deploys are getting slow
- Contributors keep stepping on each other's schema changes
Most companies never need federation, and many that adopt it do so too early.
The architecture
Client (React / iOS / Android)
│
▼
[Router] ──► Subgraph: Users (Node.js)
│ ──► Subgraph: Products (Python)
│ ──► Subgraph: Orders (Go)
│ ──► Subgraph: Reviews (PHP)
│
└── One endpoint, one unified schema- Each subgraph is a full GraphQL server managed by its team
- The router plans each query, calls the subgraphs and merges the responses
- Subgraphs can add fields to each other's entity types
- The router is the natural place for auth, rate limiting and caching
Tools: Apollo Router and Rover
# Apollo Router (a Rust binary, for production)
curl -sSL https://router.apollo.dev/download/nix/latest | sh
# Rover CLI (composes the supergraph and runs schema checks)
curl -sSL https://rover.apollo.dev/nix/latest | shApollo Gateway (a Node.js library) still exists but Apollo recommends the Router for new projects. Open-source Federation-compatible routers such as GraphQL Hive Gateway and WunderGraph Cosmo are alternatives. Check the Router's licence and which features need a paid GraphOS plan before you design around them.
Step 1 — Design subgraphs
Federation 2 schemas declare the directives they use with @link. Users subgraph:
# users/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key"])
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
createdAt: String!
}
type Query {
me: User
user(id: ID!): User
}Orders subgraph:
# orders/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key"])
type Order @key(fields: "id") {
id: ID!
total: Float!
status: String!
user: User! # cross-subgraph reference
}
# Entity stub: Users owns User; Orders adds the orders field
type User @key(fields: "id") {
id: ID!
orders: [Order!]!
}
type Query {
order(id: ID!): Order
}@key marks a type as an entity that other subgraphs can reference and extend. In Federation 2 you no longer need extend type or @external on key fields.
Step 2 — Subgraph server (Node.js example)
npm install @apollo/server @apollo/subgraph graphql graphql-tagimport { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { readFileSync } from 'node:fs';
import gql from 'graphql-tag';
const typeDefs = gql(readFileSync('./schema.graphql', 'utf-8'));
const resolvers = {
Query: {
me: async (_, __, { user }) => db.user.findById(user.id),
user: async (_, { id }) => db.user.findById(id),
},
User: {
__resolveReference: async (ref) => db.user.findById(ref.id),
},
};
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers }),
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4001, host: '10.0.0.2' }, // private address only
context: async ({ req }) => ({ user: decodeUser(req) }),
});
console.log(`Users subgraph at ${url}`);__resolveReference: when the router holds {__typename: "User", id: "42"}, it asks the Users subgraph to resolve the full User.
Step 3 — Compose and run the router
supergraph.yaml tells Rover where each subgraph lives and where its schema file is:
federation_version: =2.5.0
subgraphs:
users:
routing_url: http://10.0.0.2:4001/graphql
schema:
file: ./users/schema.graphql
orders:
routing_url: http://10.0.0.3:4002/graphql
schema:
file: ./orders/schema.graphql
products:
routing_url: http://10.0.0.4:4003/graphql
schema:
file: ./products/schema.graphqlCompose the supergraph schema:
rover supergraph compose --config supergraph.yaml > supergraph.graphqlrouter.yaml (router behaviour; key names can change between Router versions, so check the docs for yours):
supergraph:
listen: 127.0.0.1:4000 # nginx with TLS in front
path: /graphql
cors:
origins:
- https://yourcompany.com
headers:
all:
request:
- propagate:
named: authorization # forward the auth header to subgraphs
traffic_shaping:
router:
global_rate_limit:
capacity: 100
interval: 1sStart the router:
router --config router.yaml --supergraph supergraph.graphqlStep 4 — Client queries
Clients call the router endpoint only:
query MyDashboard {
me {
id
name
orders { # owned by the Orders subgraph
id
total
status
}
}
}The router:
- Queries Users for
{id, name} - Queries Orders with the user reference for
{id, total, status} - Merges the response
The client doesn't know there are multiple services.
Cross-subgraph relationships
Extending another team's entity
# Reviews subgraph
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key"])
type Review @key(fields: "id") {
id: ID!
rating: Int!
text: String!
product: Product!
}
type Product @key(fields: "id") {
id: ID!
reviews: [Review!]!
}The Products subgraph knows nothing about reviews. The Reviews subgraph adds a reviews field to Product, and the router sends product.reviews to Reviews.
@requires
When a subgraph needs a field that another subgraph owns (not the key) to compute its own field, mark it @external and use @requires on the same type:
# Orders subgraph
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.5",
import: ["@key", "@external", "@requires"])
type User @key(fields: "id") {
id: ID!
email: String! @external
receiptEmail: String! @requires(fields: "email")
orders: [Order!]!
}Before resolving receiptEmail, the router fetches email from the Users subgraph and passes it to the Orders subgraph's __resolveReference. @provides is the opposite optimisation: a subgraph declares it can return some external fields itself, saving a hop.
Auth in federation
Validate the JWT once at the router and forward the header or claims to subgraphs:
# router.yaml
authentication:
router:
jwt:
jwks:
- url: https://auth.yourcompany.com/.well-known/jwks.jsonSubgraphs can trust what the router forwards only if nothing else can reach them. Keep subgraphs on private addresses with firewall rules that allow only the router; if that isn't possible, verify the JWT in every subgraph too. Check whether the router features you rely on (JWT authentication among them) are available on your Apollo plan.
Caching
The router caches query plans in memory:
supergraph:
query_planning:
cache:
in_memory:
limit: 512Subgraphs built on Apollo Server can set cache hints with @cacheControl (declare the directive in the schema first):
type Product @key(fields: "id") @cacheControl(maxAge: 60) {
id: ID!
name: String!
price: Float! @cacheControl(maxAge: 10) # more volatile
}How those hints turn into cached responses (router response caching, a CDN, or neither) depends on your router version and plan, so test it rather than assume it.
Monitoring
Apollo GraphOS Studio (has a free tier; check current plans) shows:
- Subgraph latency
- Error rates per field
- Slow query hotspots
- Schema usage (which fields clients actually query)
The self-hosted alternative is OpenTelemetry from the router and subgraphs into Grafana Tempo, alongside Prometheus metrics. See our production observability guide.
Migration from a monolith
A gradual path:
- Keep the monolithic GraphQL schema on one server
- Put the router in front of it, with the monolith as the only subgraph
- When a team needs autonomy, move its types into a new subgraph
- Repeat: extract more subgraphs as the monolith shrinks
- Stop when the ownership boundaries feel right; you don't have to split everything
Don't "big bang" migrate. Go step by step with clear ownership.
Running this on Domain India
A federated graph is a set of long-running services, so plan it for VPS servers.
VPS 1: Apollo Router (:4000, behind nginx + TLS)
VPS 2: Users subgraph (:4001) + PostgreSQL
VPS 3: Orders subgraph (:4002) + PostgreSQL
VPS 4: Products subgraph (:4003) + PostgreSQL- Domain India VPS plans are self-managed KVM servers with full root access. Run each subgraph and the router as systemd services, on one VPS while you are small or on several as you grow.
- Only the router should be public. Connect the servers over an encrypted tunnel such as WireGuard and allow subgraph ports only from the router's address. Ask support before you rely on any private networking between VPSs.
- Shared hosting can't run the Router binary or other long-running services. A single Node.js GraphQL server may run under the Node.js app tool on cPanel or DirectAdmin, but that is a monolith, not a federated setup.
- The App Platform auto-detects Node.js apps, so it can host a Node.js subgraph; other languages or the router need your own Dockerfile. It has no WebSocket support, so GraphQL subscriptions over WebSockets won't work there.
Common pitfalls
rover subgraph check (or your router vendor's equivalent) in CI.FAQ
Apollo Federation or schema stitching?
Schema stitching (from graphql-tools) is still maintained and fine for combining a few schemas you control. Apollo Federation is the more common choice when several teams own separate parts of a graph, because ownership is declared in each subgraph's schema. For new multi-team graphs, federation is the usual choice.
Can I use Hasura Remote Schemas instead of federation?
Yes. Hasura can combine remote GraphQL endpoints with its own database-generated API, which is simpler for mixed stacks. Apollo Federation gives finer control over entity ownership but requires discipline with keys and schema checks.
Should I federate from day one?
No. Start with a monolithic schema and federate only when two or more teams need to evolve their parts of the schema independently.
Can I use federation with a single subgraph?
Yes. One subgraph behind the router still gives you the router's features, such as header handling, rate limiting and query-plan caching, and makes later splitting easier.
What is the REST-based alternative to federation?
The Backend-for-Frontend (BFF) pattern: one API per client type (web, mobile) that aggregates calls to internal REST services. For some teams it is simpler to operate than a federated graph.
Can I run a federated GraphQL stack on shared hosting?
No. The router and subgraphs are long-running services that shared hosting stops. Use one or more VPS servers; a single Node.js subgraph can also run on the App Platform.
Ready to deploy your graph? Compare VPS plans and the App Platform, or open a support ticket if you are unsure which fits.
Self-managed KVM VPS with full root access and NVMe storage, from ₹553 a month excluding GST.
Explore VPS plans