GraphQL Federation: Apollo Federation v2, Subgraphs, and Supergraph Design
Build a federated GraphQL architecture with Apollo Federation v2 — subgraph design, entity references, @key directives, supergraph schema composition, router co
GraphQL Federation: Apollo Federation v2, Subgraphs, and Supergraph Design
GraphQL Federation solves the organizational problem of GraphQL at scale: multiple teams each own their slice of the schema, and a router composes them into a unified API that clients query as if it were one service.
Without federation, a single GraphQL server forces all teams to coordinate schema changes through a bottleneck. With federation, the users team owns the User type, the orders team owns Order, and the router stitches them together at query time.
When Federation Makes Sense
Federation adds complexity. Don't use it until you have the problem it solves:
| Use It When | Avoid When |
|---|---|
| Multiple teams own different parts of the schema | Single team owns everything |
| Services already deployed independently | Starting a new project |
| You have > 3 backend services | Monolith or 2 services |
| Teams deploying schema changes block each other | Schema changes are coordinated easily |
| Different services need independent scaling | All services scale together |
For most SaaS products with < 5 engineers, a single GraphQL server is simpler and correct. Federation is for when you've outgrown it.
Core Concepts
Supergraph: The unified schema clients query against
Subgraph: An individual GraphQL service that owns part of the schema
Router: Apollo Router — composes subgraph schemas, routes queries
Entity: A type that can be referenced and extended across subgraphs
How a federated query works:
Client → Apollo Router
Router plans the query:
- Fetch User.id, User.name from Users Subgraph
- Fetch Order.id, Order.total from Orders Subgraph using User.id
- Merge results and return unified response
🌐 Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript — production-grade stack
- Fixed-price contracts — no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
Building a Subgraph
Each subgraph is an independent GraphQL server with @apollo/subgraph installed:
// services/users/src/schema.ts
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'graphql-tag';
const typeDefs = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"])
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
createdAt: String!
}
type Query {
user(id: ID!): User
me: User
}
type Mutation {
updateUserName(name: String!): User!
}
`;
const resolvers = {
User: {
// Reference resolver: called when other subgraphs need to resolve a User by ID
__resolveReference(reference: { id: string }) {
return getUserById(reference.id);
},
},
Query: {
user: (_: unknown, { id }: { id: string }) => getUserById(id),
me: (_: unknown, __: unknown, context: { userId: string }) =>
getUserById(context.userId),
},
Mutation: {
updateUserName: async (
_: unknown,
{ name }: { name: string },
context: { userId: string }
) => {
return updateUser(context.userId, { name });
},
},
};
export const schema = buildSubgraphSchema({ typeDefs, resolvers });
// services/users/src/server.ts
import Fastify from 'fastify';
import { ApolloServer } from '@apollo/server';
import fastifyApollo from '@as-integrations/fastify';
import { schema } from './schema';
const apollo = new ApolloServer({ schema });
await apollo.start();
const app = Fastify();
await app.register(fastifyApollo(apollo));
await app.listen({ port: 4001 });
console.log('Users subgraph running at http://localhost:4001/graphql');
The Orders Subgraph: Extending Another Service's Type
The Orders subgraph references User (defined in the Users subgraph) without importing its full definition:
// services/orders/src/schema.ts
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'graphql-tag';
const typeDefs = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@external", "@requires"])
# Reference the User type from Users subgraph
# Only declare fields this subgraph needs to use
type User @key(fields: "id") {
id: ID! @external
orders: [Order!]! # Orders subgraph adds this field to User
}
type Order @key(fields: "id") {
id: ID!
userId: ID!
user: User!
items: [OrderItem!]!
totalCents: Int!
status: OrderStatus!
createdAt: String!
}
type OrderItem {
productId: ID!
quantity: Int!
priceCents: Int!
}
enum OrderStatus {
PENDING
PROCESSING
SHIPPED
DELIVERED
CANCELLED
}
type Query {
order(id: ID!): Order
myOrders: [Order!]!
}
`;
const resolvers = {
User: {
orders: (user: { id: string }) => getOrdersByUserId(user.id),
},
Order: {
__resolveReference: (ref: { id: string }) => getOrderById(ref.id),
user: (order: { userId: string }) => ({ __typename: 'User', id: order.userId }),
},
Query: {
order: (_: unknown, { id }: { id: string }) => getOrderById(id),
myOrders: (_: unknown, __: unknown, ctx: { userId: string }) =>
getOrdersByUserId(ctx.userId),
},
};
export const schema = buildSubgraphSchema({ typeDefs, resolvers });
Now clients can query:
query {
me {
name
email
orders { # Resolved by Orders subgraph
id
totalCents
status
}
}
}
The router fetches me from Users, then fetches orders from Orders using the user's id — transparent to the client.
🚀 Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4–8 weeks, full platforms in 3–5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Apollo Router Configuration
Apollo Router (Rust-based, replaces Apollo Gateway) handles composition and routing:
# router.yaml
supergraph:
path: /graphql
# Subgraph definitions (in managed federation, this comes from Apollo Studio)
# For local development, use rover to compose the supergraph:
health_check:
enabled: true
path: /health
sandbox:
enabled: true # Apollo Sandbox UI at /
telemetry:
tracing:
otlp:
endpoint: http://otel-collector:4317
cors:
origins:
- https://app.yourproduct.com
allow_credentials: true
authentication:
router:
jwt:
jwks:
- url: https://your-tenant.auth0.com/.well-known/jwks.json
authorization:
preview_directives:
enabled: true
# Compose supergraph schema locally using Rover CLI
brew install rover
# rover.yaml — subgraph registry
subgraphs:
users:
routing_url: http://users-service:4001/graphql
schema:
file: ./services/users/schema.graphql
orders:
routing_url: http://orders-service:4002/graphql
schema:
file: ./services/orders/schema.graphql
# Compose and validate
rover supergraph compose --config rover.yaml > supergraph.graphql
# Start the router with composed schema
./router --config router.yaml --supergraph supergraph.graphql
Schema Design Best Practices
@key field selection:
# ✅ Use stable, immutable identifiers as @key
type User @key(fields: "id") {
id: ID!
}
# ❌ Don't use mutable fields as @key — changes break federation
type User @key(fields: "email") { # Email can change
email: String!
}
# ✅ Compound keys for junction tables
type OrderItem @key(fields: "orderId productId") {
orderId: ID!
productId: ID!
}
@shareable for types owned by multiple subgraphs:
# Money type is simple and defined in multiple subgraphs
type Money @shareable {
amountCents: Int!
currency: String!
}
Avoiding N+1 in subgraph resolvers:
// Use DataLoader to batch reference resolutions
import DataLoader from 'dataloader';
const userLoader = new DataLoader<string, User>(async (userIds) => {
const users = await db.users.findMany({
where: { id: { in: [...userIds] } }
});
// Return in same order as input IDs
return userIds.map(id => users.find(u => u.id === id) ?? new Error(`User ${id} not found`));
});
// In resolver
User: {
__resolveReference: ({ id }: { id: string }) => userLoader.load(id),
},
Migrating from Schema Stitching
If you're on schema stitching (the predecessor to federation), migration steps:
1. Add @apollo/subgraph to each service
2. Add `extend schema @link(...)` to each service's type definitions
3. Add `@key` directives to types used across services
4. Implement `__resolveReference` for each entity
5. Replace `makeRemoteExecutableSchema + mergeSchemas` with Apollo Router
6. Test with `rover subgraph check` before deploying
7. Run old and new gateway in parallel, compare query results
8. Cut over traffic, remove old gateway
Cost and Performance Comparison
| Setup | Latency Overhead | Infrastructure | Best For |
|---|---|---|---|
| Apollo Gateway (JS) | 15–30ms | Node.js service | Smaller scale, JS team |
| Apollo Router (Rust) | 1–5ms | Single binary | Production, high scale |
| Schema stitching | 20–40ms | Node.js service | Legacy; don't start new |
Apollo Router processes ~30,000 requests/second per instance on a t3.medium. For most SaaS products, a single router instance is sufficient.
Working With Viprasol
We design and implement GraphQL federation architectures — subgraph design, entity modeling, Apollo Router configuration, migration from schema stitching, and DataLoader patterns for efficient data fetching.
→ Talk to our team about GraphQL architecture and API design.
See Also
- OpenAPI Design — REST alternative for API design
- API Gateway Comparison — Apollo Router in the API gateway landscape
- Microservices Architecture — when federation becomes necessary
- API Rate Limiting — rate limiting federated GraphQL
- Web Development Services — API architecture and development
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms — we build it all with Next.js and React.
Free consultation • No commitment • Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.