Implementing GraphQL Federation for Microservices Integration
GraphQL Federation enables building a unified data graph from multiple independent GraphQL services. A client makes a single request to the Federation Gateway, which assembles data from different subgraph services and returns a unified response. Each team owns its subgraph and deploys independently.
Federation Architecture
Client (browser/mobile)
│
▼
Federation Gateway (Apollo Router / Apollo Gateway)
│
┌────┴────┬──────────┬──────────┐
▼ ▼ ▼ ▼
User Order Product Review
Subgraph Subgraph Subgraph Subgraph
(Node.js) (Go) (Python) (Node.js)
│ │ │
Postgres Postgres MongoDB
Subgraph: User Service
// user-service/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.3",
import: ["@key", "@shareable"])
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
createdAt: DateTime!
}
type Query {
me: User
user(id: ID!): User
}
`;
const resolvers = {
User: {
// Reference resolver — allows other services to extend User
__resolveReference: async ({ id }) => {
return userRepository.findById(id);
}
},
Query: {
me: (_, __, { userId }) => userRepository.findById(userId),
user: (_, { id }) => userRepository.findById(id)
}
};
export const schema = buildSubgraphSchema({ typeDefs, resolvers });
Subgraph: Order Service — Extending User Type
// order-service/schema.ts
const typeDefs = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3",
import: ["@key", "@external", "@requires"])
type Order @key(fields: "id") {
id: ID!
status: OrderStatus!
total: Float!
items: [OrderItem!]!
customer: User! # reference to type from User subgraph
createdAt: DateTime!
}
# Extend User type from another subgraph
type User @key(fields: "id") {
id: ID! @external # field belongs to User subgraph
orders(limit: Int = 10): [Order!]!
orderStats: OrderStats!
}
type OrderStats {
totalOrders: Int!
totalSpent: Float!
lastOrderAt: DateTime
}
enum OrderStatus { PENDING PAID SHIPPED DELIVERED CANCELLED }
type Query {
order(id: ID!): Order
orders(customerId: ID, status: OrderStatus): [Order!]!
}
`;
const resolvers = {
User: {
__resolveReference: async ({ id }) => ({ id }), // stub for @external fields
orders: async ({ id }, { limit }) =>
orderRepository.findByCustomerId(id, limit),
orderStats: async ({ id }) =>
orderRepository.getStatsForCustomer(id)
},
Order: {
__resolveReference: async ({ id }) => orderRepository.findById(id),
customer: ({ customerId }) => ({ __typename: 'User', id: customerId })
}
};
Apollo Router (Federation Gateway)
# router.yaml
federation_version: 2.3
supergraph:
listen: 0.0.0.0:4000
subgraphs:
users:
routing_url: http://user-service:4001/graphql
orders:
routing_url: http://order-service:4002/graphql
products:
routing_url: http://product-service:4003/graphql
cors:
origins:
- https://app.example.com
headers:
all:
request:
- propagate:
named: Authorization
- propagate:
named: X-Correlation-Id
# Run via Docker
docker run -p 4000:4000 \
-v $(pwd)/router.yaml:/dist/config/router.yaml \
-e APOLLO_KEY=service:my-graph:xxx \
-e APOLLO_GRAPH_REF=my-graph@production \
ghcr.io/apollographql/router:latest
Client Query through Federation
The client writes a query as if it's a unified graph:
query GetUserDashboard($userId: ID!) {
user(id: $userId) {
name
email
orders(limit: 5) { # data from Order subgraph
id
status
total
items {
product { # data from Product subgraph
name
imageUrl
}
quantity
}
}
orderStats {
totalOrders
totalSpent
}
}
}
The Router automatically builds an execution plan: fetch user from User subgraph, then parallelize requests to Order subgraph and data enrichment.
Managed Federation (Apollo Studio)
With Managed Federation, subgraph schemas are published to Apollo Studio Registry:
# In CI/CD pipeline
rover subgraph publish my-graph@production \
--schema ./schema.graphql \
--name orders \
--routing-url http://order-service:4002/graphql
The Router loads the current supergraph schema automatically when any subgraph changes. Publishing includes compatibility checks via rover subgraph check.
Authorization at Subgraph Level
const resolvers = {
Order: {
// Only owner or admin sees order details
__resolveReference: async ({ id }, { user }) => {
const order = await orderRepository.findById(id);
if (!order) return null;
if (order.customerId !== user.id && !user.roles.includes('admin')) {
throw new ForbiddenError('Access denied');
}
return order;
}
}
};
Implementation Timeline
- 2–3 subgraphs with basic Federation — 2–3 weeks
- Apollo Router + Managed Federation + CI compatibility checks — an additional week
- Complex @requires, @provides, nested resolvers — 1–2 additional weeks







