GraphQL Federation — Apollo Federation v2
The Problem: GraphQL at Scale
A monolithic GraphQL schema works for one team. For multiple teams owning different domains, it becomes a coordination nightmare.
Without Federation:
All teams → single schema → single service
Team A wants new User field → PR to shared schema → review bottleneck
Team B's deploy blocks on Team A's schema review
With Federation:
Team A owns users subgraph → deploys independently
Team B owns products subgraph → deploys independently
Gateway composes them into one unified schema for clientsArchitecture
Clients (web, mobile)
↓
┌──────────────────────────────────────────────┐
│ Apollo Router (Gateway) │
│ Receives queries, plans, routes to subgraphs│
│ Composes unified supergraph schema │
└────────┬──────────────┬──────────────┬────────┘
↓ ↓ ↓
┌─────────────┐ ┌──────────┐ ┌────────────┐
│Users Subgraph│ │Products │ │ Orders │
│ (Team Auth) │ │ Subgraph │ │ Subgraph │
│ │ │(Team Cat)│ │(Team Orders│
└─────────────┘ └──────────┘ └────────────┘
↓ ↓ ↓
Users DB Products DB Orders DBCore Concepts
Supergraph
The composed schema clients query — union of all subgraph schemas.
Subgraph
An independent GraphQL service that owns part of the schema. Must use the Federation spec (directives).
Entity
A type that can be referenced and extended across subgraphs. Identified by @key.
Defining a Subgraph
Users Subgraph
typescript// users-service/src/schema.ts
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'graphql-tag';
const typeDefs = gql`
# Tell federation this is the primary definition of User
type User @key(fields: "id") {
id: ID!
email: String!
name: String
createdAt: String!
}
type Query {
user(id: ID!): User
me: User
}
type Mutation {
createUser(email: String!, name: String): User
updateUser(id: ID!, name: String): User
}
`;
const resolvers = {
User: {
// __resolveReference: called by gateway when another subgraph
// references a User entity by its @key (id)
__resolveReference(reference: { id: string }) {
return db.user.findUnique({ where: { id: reference.id } });
},
},
Query: {
user: (_, { id }) => db.user.findUnique({ where: { id } }),
me: (_, __, { userId }) => db.user.findUnique({ where: { id: userId } }),
},
Mutation: {
createUser: (_, args) => db.user.create({ data: args }),
updateUser: (_, { id, ...data }) =>
db.user.update({ where: { id }, data }),
},
};
export const schema = buildSubgraphSchema({ typeDefs, resolvers });Products Subgraph (extends User)
typescript// products-service/src/schema.ts
const typeDefs = gql`
# Reference User from users subgraph — don't redefine fields
# @external marks fields owned by another subgraph
type User @key(fields: "id") {
id: ID!
# Add products field to User (owned by products subgraph)
wishlist: [Product!]!
}
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
inventory: Int!
}
type Query {
product(id: ID!): Product
products(category: String): [Product!]!
featuredProducts: [Product!]!
}
`;
const resolvers = {
User: {
__resolveReference(reference: { id: string }) {
// Products subgraph doesn't need user data — return stub
return { id: reference.id };
},
// Resolve the wishlist field that products subgraph contributes to User
wishlist: (user: { id: string }) =>
db.wishlist.findMany({ where: { userId: user.id } })
.then(items => db.product.findMany({
where: { id: { in: items.map(i => i.productId) } }
})),
},
Product: {
__resolveReference: (ref: { id: string }) =>
db.product.findUnique({ where: { id: ref.id } }),
},
Query: {
product: (_, { id }) => db.product.findUnique({ where: { id } }),
products: (_, { category }) => db.product.findMany({
where: category ? { category } : undefined,
}),
},
};Orders Subgraph (uses both User and Product)
typescriptconst typeDefs = gql`
# Reference types from other subgraphs
type User @key(fields: "id") {
id: ID!
orders: [Order!]! # Orders subgraph contributes this field to User
}
type Product @key(fields: "id") {
id: ID!
orderHistory: [OrderItem!]! # Contributed to Product type
}
type Order @key(fields: "id") {
id: ID!
user: User!
items: [OrderItem!]!
total: Float!
status: OrderStatus!
createdAt: String!
}
type OrderItem {
product: Product!
quantity: Int!
price: Float!
}
enum OrderStatus {
PENDING
PROCESSING
SHIPPED
DELIVERED
}
type Query {
order(id: ID!): Order
orders(userId: ID): [Order!]!
}
type Mutation {
placeOrder(userId: ID!, items: [OrderItemInput!]!): Order
}
input OrderItemInput {
productId: ID!
quantity: Int!
}
`;Apollo Router (Gateway) — Configuration
yaml# router.yaml
supergraph:
# Introspect subgraphs and compose (dev)
# Or use managed federation (Apollo Studio)
introspect: true
subgraphs:
users:
routing_url: http://users-service:4001/graphql
products:
routing_url: http://products-service:4002/graphql
orders:
routing_url: http://orders-service:4003/graphql
# Headers forwarding
headers:
all:
request:
- propagate:
named: authorization
- propagate:
named: x-user-idbash# Dev: compose schema from running subgraphs
npx rover dev --supergraph-config supergraph.yaml
# Or with rover compose (static)
rover supergraph compose --config supergraph.yaml > supergraph.graphql
# Start Apollo Router
./router --config router.yaml --supergraph supergraph.graphqlQuery Planning
The gateway analyzes a query and creates a query plan — deciding which subgraphs to call and in what order.
graphql# Client query
query GetUserDashboard($userId: ID!) {
user(id: $userId) {
name # from Users subgraph
email # from Users subgraph
wishlist { # from Products subgraph
name
price
}
orders { # from Orders subgraph
id
total
status
items {
product { name } # back to Products subgraph
quantity
}
}
}
}Query plan:
Fetch(users):
user(id: $userId) { name email }
Parallel:
Fetch(products) using user.id:
wishlist { name price }
Fetch(orders) using user.id:
orders { id total status items { quantity } }
→ For each order item:
Fetch(products) using item.productId:
product { name }The router handles all this orchestration — clients don't know about subgraph topology.
Directives Reference
graphql# @key — defines the entity's primary key
type User @key(fields: "id") { id: ID! }
# Composite key
type Product @key(fields: "id sku") { id: ID!, sku: String! }
# @external — field defined in another subgraph
type User @key(fields: "id") {
id: ID! @external
}
# @requires — this field resolver needs another field from this subgraph
type Product @key(fields: "id") {
id: ID!
size: String @external
weight: Float @external
shippingEstimate: Float @requires(fields: "size weight")
# Resolver receives size and weight in object
}
# @provides — optimization hint: this subgraph can provide these fields
# (avoids extra round-trip to owning subgraph)
type Order {
product: Product @provides(fields: "name price")
}
# @override — take ownership of a field from another subgraph (migration)
type Product @key(fields: "id") {
id: ID!
price: Float @override(from: "legacy-service")
# Now prices-service owns this field, legacy-service no longer queried for it
}
# @inaccessible — internal field, not exposed in supergraph
type User @key(fields: "id") {
id: ID!
internalScore: Float @inaccessible
}
# @shareable — multiple subgraphs can resolve this field
type Coordinates {
lat: Float! @shareable
lng: Float! @shareable
}Managed Federation (Apollo Studio)
In production, instead of a static supergraph.yaml, subgraphs publish their schemas to Apollo Studio.
bash# In CI/CD for each subgraph:
rover subgraph publish my-graph@production \
--name users \
--schema schema.graphql \
--routing-url https://users-service.prod/graphqlApollo Studio:
- Validates new schema doesn't break existing queries
- Composes new supergraph
- Pushes hot-reload config to running routers (no restart needed)
This enables schema checks — fail CI if a change breaks existing client queries.
bash# Check before publish
rover subgraph check my-graph@production \
--name users \
--schema schema.graphqlvs Schema Stitching
| Aspect | Apollo Federation | Schema Stitching |
|---|---|---|
| Approach | Spec-based, annotations in schema | Config-based, external merging |
| Ownership model | Each type owned/extended by subgraph | Configured at gateway level |
__resolveReference |
Yes — subgraph resolves its own entities | No — gateway resolves |
| Performance | Query planning built-in | Manual batching with DataLoader |
| Adoption | Industry standard | Less common for new projects |
| Incremental migration | @override directive |
Awkward |
Interview Questions
Q: What is GraphQL Federation and why use it? Federation allows multiple independent GraphQL services (subgraphs) to compose into a single schema for clients. Each team owns their domain schema and deploys independently. The gateway (Apollo Router) composes the schemas and routes queries. Use it when multiple teams need to contribute to the same GraphQL API without schema coordination bottlenecks.
Q: What is an entity in GraphQL Federation?
An entity is a type that can be referenced across subgraphs, defined with @key directive specifying its unique identifier(s). Each subgraph that references the entity must implement __resolveReference to resolve the entity from its key. The gateway uses __resolveReference to stitch together data from multiple subgraphs in a single query.
Q: How does the gateway know which subgraph to call?
The Apollo Router builds a query plan by analyzing the query against the composed supergraph schema. It knows which fields belong to which subgraph (tracked during composition). For entity references (e.g., user.orders where User is from users-subgraph and orders is from orders-subgraph), it fetches the User first, extracts the @key fields (id), then passes that to the orders-subgraph as a _representations query.
Q: What is @requires and when do you use it?
@requires declares that a field resolver needs additional fields from the same entity to compute its value. For example, shippingEstimate @requires(fields: "weight dimensions") tells the gateway to include weight and dimensions in the subgraph query so the resolver has them available. It's an optimization hint that prevents unnecessary extra round-trips.