# GraphQL Federation — Multi-Service Schemas with Apollo Federation
TL;DR
When your app grows past 5-10 teams, a single monolithic GraphQL schema becomes a bottleneck. Federation lets each team own a subgraph; a gateway composes them into a unified graph. This guide covers Apollo Federation v2 setup, subgraph design, gateway deployment on DomainIndia VPS, and common pitfalls.
## 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:
```bash
# Router (Rust, high performance — recommended for production)
curl -sSL https://router.apollo.dev/download/nix/latest | sh
```
Alternative: Apollo Gateway (Node.js — simpler for development).
## Step 1 — Design subgraphs
Users subgraph:
```graphql
# 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:
```graphql
# 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)
```bash
npm install @apollo/subgraph graphql
```
```typescript
import { 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`:
```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: 100
```
Compose the supergraph SDL:
```bash
rover supergraph compose --config supergraph.yaml > supergraph.graphql
```
Start router:
```bash
router --config router.yaml --supergraph supergraph.graphql
```
## Step 4 — Client queries
Clients hit the gateway endpoint only:
```graphql
query MyDashboard {
me {
id
name
orders { # owned by Orders subgraph
id
total
status
}
}
}
```
The gateway orchestrates:
1. Query Users for `{id, name}`
2. Query Orders with the user ID for `{id, total, status}`
3. Compose response
Client doesn't know there are multiple services.
## Cross-subgraph relationships
### Key entities
```graphql
# 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:
```graphql
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-products
```
Internal network (Cloudflare Tunnel or private subnet) for subgraph traffic. Only router exposed publicly.
## Auth in federation
JWT validated at router, claims forwarded to subgraphs:
```yaml
# router.yaml
authentication:
router:
jwt:
jwks:
- url: https://auth.yourcompany.com/.well-known/jwks.json
```
Subgraphs trust the router's auth context — don't re-verify JWT per subgraph.
## Caching
Apollo Router has query-plan caching:
```yaml
supergraph:
query_planning:
cache:
in_memory:
limit: 512
```
Responses can be cached per-field with `@cacheControl(maxAge: 60)`:
```graphql
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:
1. Start with a monolithic GraphQL schema on 1 server
2. When a team needs autonomy, extract their types into a subgraph
3. Add the first subgraph router between clients and the monolith
4. Repeat: extract more subgraphs, monolith shrinks
5. Eventually: pure federated architecture
Don't "big bang" migrate. Incremental with clear ownership.
## Common pitfalls
## FAQ
Q
Apollo Federation or GraphQL stitching?
Stitching is legacy (Apollo v1 approach). Federation v2 is the modern standard. New projects use federation.
Q
Hasura Remote Schemas instead of federation?
Hasura federates any GraphQL endpoint — simpler for mixed technology stacks. Apollo Federation gives more control (requires @key discipline).
Q
Monolith or federate from day one?
Monolith. Federate only when 2+ teams independently evolve the schema.
Q
Can I use Federation with a single subgraph?
Yes — even one subgraph + router gets you router features (rate limiting, auth, caching). Pragmatic starting point.
Q
REST alternative to federation?
BFF pattern (Backend-for-Frontend) — one GraphQL API per client (web, mobile). Each BFF aggregates REST calls. Simpler ops than federation for some teams.