GraphQL Federation — Multi-Service Schemas with Apollo Federation
When to federate
Don't start here. Federation adds operational complexity.
Stay monolithic when:
- Single team owns the schema
- <20 engineers touching GraphQL
- 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 step on each other's schemas
Most companies never need federation. Most that adopt it, adopt too early.
The architecture
Client (React / iOS / Android)
│
▼
[Gateway / 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
- Gateway queries subgraphs, composes responses
- Subgraphs can extend each other's types
- Gateway handles auth, rate limiting, caching
Apollo Federation v2
Install Apollo Gateway + Router:
# Router (Rust, high performance — recommended for production)
curl -sSL https://router.apollo.dev/download/nix/latest | shAlternative: Apollo Gateway (Node.js — simpler for development).
Step 1 — Design subgraphs
Users subgraph:
# users/schema.graphql
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
createdAt: String!
}
extend type Query {
me: User
user(id: ID!): User
}Orders subgraph:
# orders/schema.graphql
type Order @key(fields: "id") {
id: ID!
total: Float!
status: String!
user: User! # cross-subgraph reference
}
extend type User @key(fields: "id") {
id: ID! @external # declared in Users subgraph
orders: [Order!]! # new field on User owned by Orders subgraph
}
extend type Query {
order(id: ID!): Order
}@key marks a type as federatable. extend type adds fields to a type owned by another subgraph.
Step 2 — Subgraph server (Node.js example)
npm install @apollo/subgraph graphqlimport { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { readFileSync } from 'fs';
import { parse } from 'graphql';
const typeDefs = parse(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 },
context: async ({ req }) => ({ user: decodeUser(req) }),
});
console.log(`Users subgraph at ${url}`);__resolveReference — when the gateway has a {__typename: "User", id: "42"}, it asks the Users subgraph to resolve full User.
Step 3 — Gateway (Apollo Router)
router.yaml:
supergraph:
listen: 0.0.0.0:4000
path: /graphql
subgraphs:
users:
routing_url: http://users-service.internal:4001/graphql
orders:
routing_url: http://orders-service.internal:4002/graphql
products:
routing_url: http://products-service.internal:4003/graphql
cors:
origins:
- https://yourcompany.com
headers:
all:
request:
- propagate:
named: authorization # forward auth header to subgraphs
rate_limiting:
global:
interval: 1s
capacity: 100Compose the supergraph SDL:
rover supergraph compose --config supergraph.yaml > supergraph.graphqlStart router:
router --config router.yaml --supergraph supergraph.graphqlStep 4 — Client queries
Clients hit the gateway endpoint only:
query MyDashboard {
me {
id
name
orders { # owned by Orders subgraph
id
total
status
}
}
}The gateway orchestrates:
- Query Users for
{id, name} - Query Orders with the user ID for
{id, total, status} - Compose response
Client doesn't know there are multiple services.
Cross-subgraph relationships
Key entities
# Reviews subgraph
type Review @key(fields: "id") {
id: ID!
rating: Int!
text: String!
product: Product!
}
extend type Product @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
}Products subgraph doesn't know about Reviews. Reviews subgraph extends Product with a reviews field. The gateway routes product.reviews to the Reviews subgraph.
@requires and @provides
When Orders needs a User field that's NOT the key:
extend type User @key(fields: "id") {
id: ID! @external
email: String @external
}
type Order @key(fields: "id") {
id: ID!
notificationEmail: String @requires(fields: "email")
user: User!
}@requires(fields: "email") — before resolving notificationEmail, gateway fetches email from Users subgraph.
Deployment on DomainIndia
Each subgraph as its own systemd service on a VPS (or separate VPS).
Router as separate service.
VPS 1: Apollo Router (:4000)
VPS 2: Users subgraph (:4001) + Postgres-users
VPS 3: Orders subgraph (:4002) + Postgres-orders
VPS 4: Products subgraph (:4003) + Postgres-productsInternal network (Cloudflare Tunnel or private subnet) for subgraph traffic. Only router exposed publicly.
Auth in federation
JWT validated at router, claims forwarded to subgraphs:
# router.yaml
authentication:
router:
jwt:
jwks:
- url: https://auth.yourcompany.com/.well-known/jwks.jsonSubgraphs trust the router's auth context — don't re-verify JWT per subgraph.
Caching
Apollo Router has query-plan caching:
supergraph:
query_planning:
cache:
in_memory:
limit: 512Responses can be cached per-field with @cacheControl(maxAge: 60):
type Product @key(fields: "id") @cacheControl(maxAge: 60) {
id: ID!
name: String!
price: Float! @cacheControl(maxAge: 10) # more volatile
}Monitoring
Apollo Studio (paid — free tier) gives visibility:
- Subgraph latency
- Error rates per field
- Slow query hotspots
- Schema usage (which fields are actually queried)
Alternative: self-hosted with OpenTelemetry → Grafana Tempo.
Migration from monolith
Gradual path:
- Start with a monolithic GraphQL schema on 1 server
- When a team needs autonomy, extract their types into a subgraph
- Add the first subgraph router between clients and the monolith
- Repeat: extract more subgraphs, monolith shrinks
- Eventually: pure federated architecture
Don't "big bang" migrate. Incremental with clear ownership.
Common pitfalls
FAQ
Stitching is legacy (Apollo v1 approach). Federation v2 is the modern standard. New projects use federation.
Hasura federates any GraphQL endpoint — simpler for mixed technology stacks. Apollo Federation gives more control (requires @key discipline).
Monolith. Federate only when 2+ teams independently evolve the schema.
Yes — even one subgraph + router gets you router features (rate limiting, auth, caching). Pragmatic starting point.
BFF pattern (Backend-for-Frontend) — one GraphQL API per client (web, mobile). Each BFF aggregates REST calls. Simpler ops than federation for some teams.
Host a federated GraphQL stack on multiple DomainIndia VPSes. Explore VPS plans