GraphQL Federation: Scaling APIs Across Distributed Teams in 2026
Your company has 15 microservices, each owned by a different team. The frontend needs data from 6 of them to render a single page. Do you make 6 separate REST calls? Build a BFF (Backend for Frontend) that aggregates them? Or do you give the frontend a single, unified GraphQL API that hides the complexity of your service architecture?
GraphQL Federation is the answer to that third option — and in 2026, it has matured from experimental to production-ready at companies like Netflix, Expedia, Walmart, and GitHub.
What Is GraphQL Federation
Federation lets you compose multiple GraphQL services (called subgraphs) into a single, unified API (called a supergraph). Each team owns their subgraph independently, and a router stitches them together at runtime.
┌─────────────────────────────────────────┐
│ Frontend │
│ (Single GraphQL Endpoint) │
└────────────────┬────────────────────────-┘
│
┌────────┴────────┐
│ Apollo Router │ ← Query planning & execution
└────┬────┬────┬──┘
│ │ │
┌───────┘ │ └───────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Users │ │ Orders │ │Products │ ← Subgraphs (owned by different teams)
│ Subgraph │ │Subgraph │ │Subgraph │
└─────────┘ └─────────┘ └─────────┘
Building a Federated Subgraph
Each team defines their subgraph with standard GraphQL schema, plus federation-specific directives:
# users-subgraph/schema.graphql
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
role: UserRole!
createdAt: DateTime!
}
enum UserRole {
ADMIN
CUSTOMER
VENDOR
}
type Query {
user(id: ID!): User
users(limit: Int = 20, offset: Int = 0): [User!]!
}
// users-subgraph/resolvers.ts
import { buildSubgraphSchema } from "@apollo/subgraph";
import { ApolloServer } from "@apollo/server";
const resolvers = {
Query: {
user: (_, { id }) => userService.findById(id),
users: (_, { limit, offset }) => userService.findAll(limit, offset),
},
User: {
// Federation reference resolver — how to fetch a User by key
__resolveReference: (ref) => userService.findById(ref.id),
},
};
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers }),
});
The @key(fields: "id") directive tells the router: "If another subgraph references a User by its id, I know how to resolve it."
Extending Types Across Subgraphs
This is where federation shines. The orders team can extend the User type without modifying the users subgraph:
# orders-subgraph/schema.graphql
type Order @key(fields: "id") {
id: ID!
status: OrderStatus!
total: Float!
items: [OrderItem!]!
placedAt: DateTime!
}
# Extend User from the users subgraph
type User @key(fields: "id") {
id: ID!
orders: [Order!]! # Added by orders team
totalSpent: Float! # Added by orders team
}
type Query {
order(id: ID!): Order
ordersByStatus(status: OrderStatus!): [Order!]!
}
// orders-subgraph/resolvers.ts
const resolvers = {
User: {
orders: (user) => orderService.findByUserId(user.id),
totalSpent: (user) => orderService.calculateTotalSpent(user.id),
},
Order: {
__resolveReference: (ref) => orderService.findById(ref.id),
},
};
The frontend can now query:
query GetUserWithOrders {
user(id: "123") {
name # Resolved by users subgraph
email # Resolved by users subgraph
orders { # Resolved by orders subgraph
id
status
total
}
totalSpent # Resolved by orders subgraph
}
}
One query, two services, zero coordination needed between teams.
The Apollo Router: Query Planning
The router receives the query, breaks it into subgraph operations, executes them in parallel where possible, and assembles the response:
# router.yaml — Apollo Router configuration
supergraph:
listen: 0.0.0.0:4000
subgraphs:
users:
routing_url: http://users-service:4001/graphql
orders:
routing_url: http://orders-service:4002/graphql
products:
routing_url: http://products-service:4003/graphql
# Performance tuning
traffic_shaping:
all:
timeout: 30s
subgraphs:
users:
timeout: 5s
orders:
timeout: 10s
# Rate limiting
limits:
max_depth: 15
max_height: 200
max_aliases: 30
# Caching
preview_entity_cache:
enabled: true
subgraph:
all:
ttl: 60s
# Enable query plans for debugging
plugins:
experimental.expose_query_plan: true
For the query above, the router generates this execution plan:
1. Fetch User(id: "123") from users subgraph → { id, name, email }
2. In parallel:
a. Fetch User.orders from orders subgraph → [{ id, status, total }]
b. Fetch User.totalSpent from orders subgraph → Float
3. Merge results and return
Steps 2a and 2b execute in parallel because they go to the same subgraph. The router optimizes this automatically.
Schema Governance and Composition
When multiple teams contribute to a shared schema, you need governance to prevent breaking changes:
# Check schema compatibility before deploying
rover subgraph check my-graph@production \
--schema ./schema.graphql \
--name orders-subgraph
# Output:
# ✓ No breaking changes detected
# ⚠ 1 warning: Field Order.legacyId is deprecated
# ✓ Composition successful
Schema checks run in CI/CD pipelines before any subgraph deployment. They catch:
Breaking removals (removing a field that clients use)
Type conflicts (two subgraphs defining incompatible types)
Composition errors (circular dependencies, missing key fields)
Performance Optimization: The N+1 Problem
Federation introduces the entity resolution pattern, which can cause N+1 query problems. If you fetch 20 orders and each needs a User, naive resolution calls the users subgraph 20 times.
The solution is DataLoader batching:
import DataLoader from "dataloader";
// Batch user lookups — called once with all IDs
const userLoader = new DataLoader(async (userIds: string[]) => {
const users = await userService.findByIds(userIds);
const userMap = new Map(users.map((u) => [u.id, u]));
return userIds.map((id) => userMap.get(id) || null);
});
const resolvers = {
Order: {
customer: (order) => userLoader.load(order.customerId),
},
};
The router also supports entity batching natively. Instead of 20 individual reference resolver calls, it sends a single batch request:
# Single batched request from router to users subgraph
query {
_entities(representations: [
{ __typename: "User", id: "1" },
{ __typename: "User", id: "2" },
# ... up to 20 users in one call
]) {
... on User { id, name, email }
}
}
Federation vs REST Aggregation vs BFF
| Aspect | GraphQL Federation | REST + BFF | REST Direct |
|---|---|---|---|
| Frontend queries | 1 endpoint | 1 endpoint | N endpoints |
| Team independence | High (own subgraph) | Low (shared BFF) | High (own API) |
| Over-fetching | None (client selects fields) | Moderate | High |
| Schema governance | Built-in (composition checks) | Manual | None |
| Learning curve | Moderate | Low | Low |
| Tooling maturity | Excellent (2026) | Varies | Excellent |
| Real-time (subscriptions) | Supported | Custom | WebSocket per service |
When to Choose Federation
Good fit:
Multiple teams owning different domains (users, orders, inventory)
Frontend teams that need flexible data fetching
Rapid frontend iteration without backend changes
Complex data relationships spanning multiple services
Poor fit:
Simple CRUD APIs with one or two services
Machine-to-machine APIs where payload shape is fixed
Teams without GraphQL experience and short timelines
Write-heavy APIs (GraphQL mutations can be awkward for complex workflows)
Production Checklist
Schema governance — Run composition checks in CI for every subgraph change
Query depth limiting — Prevent malicious deeply nested queries
Persisted queries — Allowlist known queries in production for security
Response caching — Cache entity lookups at the router level
Error handling — Partial responses (some subgraphs fail, others succeed) must be handled gracefully
Observability — Trace each query through the router to every subgraph it touches
Schema documentation — Every field should have a description; this is your API contract
GraphQL Federation solves a real organizational problem: how do you give frontend teams a unified, flexible API without creating a monolithic gateway or forcing backend teams to coordinate on every change? The answer is a composed supergraph where each team owns their slice of the schema. In 2026, the tooling has caught up with the vision, and federation is ready for production.