Overview
Schema federation allows multiple GraphQL services to compose into a single unified schema. It’s particularly useful in microservices architectures where different teams or services own different parts of the schema.
Federation Concepts
Subgraphs
- Definition: Individual GraphQL services with their own schemas
- Ownership: Each subgraph is owned by a specific team or service
- Independence: Subgraphs can be developed and deployed independently
Gateway
- Definition: Central GraphQL server that composes subgraphs
- Composition: Combines subgraph schemas into a unified schema
- Routing: Routes queries to appropriate subgraphs
Entities
- Definition: Types that can be extended across subgraphs
- Keys: Unique identifiers for cross-subgraph references
- Extensions: Adding fields to entities from other subgraphs
Federation Directives
@key
- Purpose: Defines entity keys for cross-subgraph references
- Usage: Marks fields that uniquely identify an entity
- Multiple Keys: Entities can have multiple keys
type User @key(fields: "id") @key(fields: "email") {
id: ID!
email: String!
name: String!
}@extends
- Purpose: Extends an entity defined in another subgraph
- Usage: Adds fields to entities from other subgraphs
- Requirements: Must include @key directive
type User @extends @key(fields: "id") {
id: ID! @external
posts: [Post!]!
}@external
- Purpose: Marks fields that are defined in other subgraphs
- Usage: References fields from extended entities
- Resolution: Fields are resolved by the originating subgraph
type User @extends @key(fields: "id") {
id: ID! @external
email: String! @external
profile: Profile
}@provides
- Purpose: Indicates fields provided by this subgraph
- Usage: Optimizes query planning
- Context: Used with entity extensions
type User @extends @key(fields: "id") {
id: ID! @external
posts: [Post!]! @provides(fields: "author")
}@requires
- Purpose: Indicates fields needed from other subgraphs
- Usage: Access fields from extended entities
- Dependencies: Creates subgraph dependencies
type Review @key(fields: "id") {
id: ID!
product: Product! @provides(fields: "reviews")
rating: Int! @requires(fields: "product { category }")
}Subgraph Architecture
Service Definition
// users subgraph
const { ApolloServer } = require('apollo-server');
const { buildSubgraphSchema } = require('@apollo/subgraph');
const typeDefs = gql`
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
`;
const resolvers = {
User: {
__resolveReference(reference) {
return getUserById(reference.id);
}
}
};
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers })
});Entity Resolution
const resolvers = {
User: {
__resolveReference(reference, context, info) {
// Resolve entity by key
if (reference.id) {
return getUserById(reference.id);
}
if (reference.email) {
return getUserByEmail(reference.email);
}
throw new Error('Unable to resolve User reference');
},
// Regular field resolvers
posts: (parent, args, context, info) => {
return getPostsByUserId(parent.id);
}
}
};Cross-Subgraph References
// posts subgraph
const typeDefs = gql`
type Post @key(fields: "id") {
id: ID!
title: String!
content: String!
authorId: ID!
author: User!
}
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post!]!
}
`;
const resolvers = {
Post: {
author: (parent, args, context, info) => {
// Reference resolution handled by gateway
return { __typename: 'User', id: parent.authorId };
}
},
User: {
posts: (parent, args, context, info) => {
return getPostsByUserId(parent.id);
}
}
};Gateway Configuration
Apollo Gateway Setup
const { ApolloGateway, RemoteGraphQLDataSource } = require('@apollo/gateway');
const gateway = new ApolloGateway({
serviceList: [
{ name: 'users', url: 'http://localhost:4001' },
{ name: 'posts', url: 'http://localhost:4002' },
{ name: 'reviews', url: 'http://localhost:4003' }
],
buildService({ name, url }) {
return new RemoteGraphQLDataSource({
url,
willSendRequest({ request, context }) {
// Forward authentication headers
request.http.headers.set('authorization', context.authToken);
}
});
}
});
const server = new ApolloServer({
gateway,
subscriptions: false
});Schema Composition
// Automatic composition
const gateway = new ApolloGateway({
serviceList: serviceList,
// Composition happens automatically
});
// Manual composition (for advanced cases)
const { composeServices } = require('@apollo/composition');
const composedSchema = composeServices([
{ name: 'users', typeDefs: usersSchema },
{ name: 'posts', typeDefs: postsSchema }
]);Advanced Patterns
Value Types
- Definition: Types that don’t need @key directive
- Usage: Simple data structures shared across subgraphs
- Composition: Included in all subgraphs that use them
# Shared value type
type Address {
street: String!
city: String!
country: String!
}
type User @key(fields: "id") {
id: ID!
name: String!
address: Address!
}Interface Extensions
# Base interface
interface Node {
id: ID!
}
# Extending interface
type User implements Node @key(fields: "id") {
id: ID!
name: String!
}
type Post implements Node @key(fields: "id") {
id: ID!
title: String!
}Federation Versioning
# Versioned entities
type User_v1 @key(fields: "id") {
id: ID!
name: String!
}
type User_v2 @key(fields: "id") {
id: ID!
name: String!
email: String!
}Query Planning
Query Decomposition
- Analysis: Gateway analyzes incoming query
- Decomposition: Breaks query into subgraph operations
- Execution: Runs operations in parallel
- Composition: Combines results into final response
Execution Flow
Client Query
↓
Gateway Analysis
↓
Query Planning
↓
Parallel Execution
↓
Result Composition
↓
Client Response
Optimization
- Batching: Combine multiple requests to same subgraph
- Caching: Cache entity references
- Prefetching: Preload commonly accessed entities
Error Handling
Subgraph Errors
const resolvers = {
User: {
posts: async (parent, args, context, info) => {
try {
return await getPostsByUserId(parent.id);
} catch (error) {
// Log error but don't fail entire query
console.error('Failed to load posts:', error);
return []; // Return empty array
}
}
}
};Partial Results
- Graceful Degradation: Return partial results when possible
- Error Propagation: Include subgraph errors in response
- Fallback Values: Provide default values for failed fields
Testing Federation
Subgraph Testing
const { createTestClient } = require('apollo-server-testing');
describe('Users Subgraph', () => {
const { query } = createTestClient(usersServer);
it('should resolve user reference', async () => {
const result = await query({
query: `
query($representations: [_Any!]!) {
_entities(representations: $representations) {
...on User {
id
name
}
}
}
`,
variables: {
representations: [{ __typename: 'User', id: '1' }]
}
});
expect(result.data._entities[0].name).toBe('John Doe');
});
});Federation Testing
const { ApolloServer } = require('apollo-server');
const { ApolloGateway } = require('@apollo/gateway');
describe('Federated Schema', () => {
let gateway;
beforeAll(async () => {
gateway = new ApolloGateway({
serviceList: [
{ name: 'users', url: 'http://localhost:4001' },
{ name: 'posts', url: 'http://localhost:4002' }
]
});
await gateway.load();
});
it('should compose schemas successfully', () => {
expect(gateway.schema).toBeDefined();
});
});Performance Considerations
N+1 Query Problem
- Cause: Multiple round trips for entity resolution
- Solution: Use DataLoader in subgraphs
- Optimization: Batch entity resolution
Network Latency
- Impact: Cross-subgraph calls add latency
- Mitigation: Colocate related subgraphs
- Caching: Cache entity references
Schema Complexity
- Composition Time: Large schemas take longer to compose
- Query Planning: Complex queries require more planning
- Optimization: Keep subgraph schemas focused
Deployment Strategies
Independent Deployment
- Subgraphs: Deploy independently
- Gateway: Update service list
- Compatibility: Maintain backward compatibility
Blue-Green Deployment
- Subgraph Updates: Deploy new version alongside old
- Traffic Shifting: Gradually move traffic
- Rollback: Quick reversion capability
Schema Registry
- Storage: Store composed schemas
- Validation: Validate changes against registry
- Approval: Require approval for breaking changes
Tooling
Apollo Studio
- Schema Registry: Store and version schemas
- Composition: Automatic schema composition
- Analytics: Query performance and usage
- Linting: Schema validation and best practices
Rover CLI
# Publish subgraph schema
rover subgraph publish my-graph@current \
--schema ./subgraph-schema.graphql \
--name users \
--routing-url http://users-service.com
# Check composition
rover subgraph check my-graph@current \
--schema ./subgraph-schema.graphql \
--name usersGraphQL Mesh
- Alternative: Schema stitching approach
- Sources: Multiple data sources (REST, GraphQL, etc.)
- Transformation: Transform and merge schemas
Best Practices
Subgraph Design
- Domain Boundaries: Align subgraphs with domain boundaries
- Team Ownership: One team per subgraph
- Size: Keep subgraphs manageable
- Independence: Minimize cross-subgraph dependencies
Entity Design
- Keys: Choose stable, unique keys
- Extensions: Use @extends for cross-cutting concerns
- Ownership: Clear ownership of entities
Performance
- DataLoader: Use in all subgraphs
- Caching: Implement appropriate caching
- Monitoring: Monitor subgraph performance
Governance
- Review Process: Review schema changes
- Breaking Changes: Plan for breaking changes
- Documentation: Document subgraph responsibilities