GraphQL Federation: Building Scalable Microservices with Unified Graph APIs

As organizations scale their applications, the microservices architecture has become the de facto standard for building maintainable, scalable systems. However, this approach often leads to API sprawl and complexity for frontend developers. GraphQL Federation emerges as a powerful solution, enabling teams to build a unified GraphQL API while maintaining the autonomy and benefits of microservices.

Understanding GraphQL Federation

GraphQL Federation allows you to compose a distributed graph from multiple GraphQL services. Each service owns its part of the graph and can be developed, deployed, and scaled independently while presenting a single, unified API to clients.

Traditional Microservices vs. Federation

Traditional Approach:              Federation Approach:
┌─────────────┐                   ┌─────────────────┐
│   Client    │                   │     Client      │
└──────┬──────┘                   └────────┬────────┘
       │                                    │
   ┌───┴────┐                              │
   │Gateway │                              │
   └───┬────┘                      ┌───────┴────────┐
  ┌────┼────┬─────┐               │ Apollo Gateway │
  │    │    │     │               └───────┬────────┘
┌─┴─┐┌─┴─┐┌─┴─┐┌──┴──┐                   │
│API││API││API││ API │           ┌────────┼────────┐
└───┘└───┘└───┘└─────┘           │        │        │
                               ┌──┴──┐  ┌──┴──┐  ┌─┴──┐
                               │Users│  │Posts│  │Comm│
                               └─────┘  └─────┘  └────┘

Setting Up GraphQL Federation

1. Gateway Service

First, let's create the Apollo Gateway that will compose our federated graph:

// gateway/index.js
import { ApolloServer } from '@apollo/server';
import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
import { startStandaloneServer } from '@apollo/server/standalone';

const gateway = new ApolloGateway({
  supergraphSdl: new IntrospectAndCompose({
    subgraphs: [
      { name: 'users', url: 'http://localhost:4001/graphql' },
      { name: 'posts', url: 'http://localhost:4002/graphql' },
      { name: 'comments', url: 'http://localhost:4003/graphql' },
    ],
  }),
});

const server = new ApolloServer({
  gateway,
  subscriptions: false,
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
});

console.log(`🚀 Gateway ready at ${url}`);

2. User Service

Create the first federated service for user management:

// services/users/index.js
import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { startStandaloneServer } from '@apollo/server/standalone';
import gql from 'graphql-tag';

const typeDefs = gql`
  extend schema
    @link(url: "https://specs.apollo.dev/federation/v2.0",
          import: ["@key", "@shareable", "@external", "@provides"])

  type User @key(fields: "id") {
    id: ID!
    username: String!
    email: String!
    profile: UserProfile!
    createdAt: String!
  }

  type UserProfile {
    bio: String
    avatar: String
    location: String
  }

  type Query {
    user(id: ID!): User
    users: [User!]!
    currentUser: User
  }

  type Mutation {
    createUser(input: CreateUserInput!): User!
    updateUser(id: ID!, input: UpdateUserInput!): User!
  }

  input CreateUserInput {
    username: String!
    email: String!
    profile: ProfileInput
  }

  input UpdateUserInput {
    username: String
    email: String
    profile: ProfileInput
  }

  input ProfileInput {
    bio: String
    avatar: String
    location: String
  }
`;

const resolvers = {
  User: {
    __resolveReference(user) {
      return users.find(u => u.id === user.id);
    },
  },
  Query: {
    user: (_, { id }) => users.find(user => user.id === id),
    users: () => users,
    currentUser: (_, __, context) => {
      return users.find(user => user.id === context.userId);
    },
  },
  Mutation: {
    createUser: (_, { input }) => {
      const user = {
        id: String(users.length + 1),
        ...input,
        createdAt: new Date().toISOString(),
      };
      users.push(user);
      return user;
    },
    updateUser: (_, { id, input }) => {
      const index = users.findIndex(user => user.id === id);
      if (index === -1) throw new Error('User not found');
      
      users[index] = { ...users[index], ...input };
      return users[index];
    },
  },
};

// In-memory data store
const users = [
  {
    id: '1',
    username: 'alice',
    email: '[email protected]',
    profile: {
      bio: 'Software Engineer',
      avatar: 'https://example.com/alice.jpg',
      location: 'San Francisco',
    },
    createdAt: '2024-01-01T00:00:00Z',
  },
];

const server = new ApolloServer({
  schema: buildSubgraphSchema([{ typeDefs, resolvers }]),
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4001 },
});

console.log(`🚀 Users service ready at ${url}`);

3. Posts Service

Create a service that references users:

// services/posts/index.js
import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { startStandaloneServer } from '@apollo/server/standalone';
import gql from 'graphql-tag';

const typeDefs = gql`
  extend schema
    @link(url: "https://specs.apollo.dev/federation/v2.0",
          import: ["@key", "@shareable", "@external", "@requires"])

  type Post @key(fields: "id") {
    id: ID!
    title: String!
    content: String!
    author: User!
    tags: [String!]!
    publishedAt: String
    updatedAt: String!
  }

  extend type User @key(fields: "id") {
    id: ID! @external
    posts: [Post!]!
    postCount: Int!
  }

  type Query {
    post(id: ID!): Post
    posts(limit: Int = 10, offset: Int = 0): PostConnection!
    postsByUser(userId: ID!): [Post!]!
    searchPosts(query: String!): [Post!]!
  }

  type PostConnection {
    nodes: [Post!]!
    totalCount: Int!
    pageInfo: PageInfo!
  }

  type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    endCursor: String
    startCursor: String
  }

  type Mutation {
    createPost(input: CreatePostInput!): Post!
    updatePost(id: ID!, input: UpdatePostInput!): Post!
    deletePost(id: ID!): Boolean!
    publishPost(id: ID!): Post!
  }

  input CreatePostInput {
    title: String!
    content: String!
    tags: [String!]
  }

  input UpdatePostInput {
    title: String
    content: String
    tags: [String!]
  }
`;

const resolvers = {
  Post: {
    __resolveReference(post) {
      return posts.find(p => p.id === post.id);
    },
    author(post) {
      return { __typename: 'User', id: post.authorId };
    },
  },
  User: {
    posts(user) {
      return posts.filter(post => post.authorId === user.id);
    },
    postCount(user) {
      return posts.filter(post => post.authorId === user.id).length;
    },
  },
  Query: {
    post: (_, { id }) => posts.find(post => post.id === id),
    posts: (_, { limit, offset }) => {
      const paginatedPosts = posts.slice(offset, offset + limit);
      return {
        nodes: paginatedPosts,
        totalCount: posts.length,
        pageInfo: {
          hasNextPage: offset + limit < posts.length,
          hasPreviousPage: offset > 0,
          endCursor: Buffer.from(`${offset + limit}`).toString('base64'),
          startCursor: Buffer.from(`${offset}`).toString('base64'),
        },
      };
    },
    postsByUser: (_, { userId }) => 
      posts.filter(post => post.authorId === userId),
    searchPosts: (_, { query }) => 
      posts.filter(post => 
        post.title.toLowerCase().includes(query.toLowerCase()) ||
        post.content.toLowerCase().includes(query.toLowerCase())
      ),
  },
  Mutation: {
    createPost: (_, { input }, context) => {
      const post = {
        id: String(posts.length + 1),
        ...input,
        authorId: context.userId,
        publishedAt: null,
        updatedAt: new Date().toISOString(),
      };
      posts.push(post);
      return post;
    },
    updatePost: (_, { id, input }) => {
      const index = posts.findIndex(post => post.id === id);
      if (index === -1) throw new Error('Post not found');
      
      posts[index] = {
        ...posts[index],
        ...input,
        updatedAt: new Date().toISOString(),
      };
      return posts[index];
    },
    deletePost: (_, { id }) => {
      const index = posts.findIndex(post => post.id === id);
      if (index === -1) return false;
      
      posts.splice(index, 1);
      return true;
    },
    publishPost: (_, { id }) => {
      const post = posts.find(p => p.id === id);
      if (!post) throw new Error('Post not found');
      
      post.publishedAt = new Date().toISOString();
      return post;
    },
  },
};

// In-memory data store
const posts = [
  {
    id: '1',
    title: 'Introduction to GraphQL Federation',
    content: 'GraphQL Federation is a powerful architecture...',
    authorId: '1',
    tags: ['graphql', 'federation', 'microservices'],
    publishedAt: '2024-01-15T00:00:00Z',
    updatedAt: '2024-01-15T00:00:00Z',
  },
];

const server = new ApolloServer({
  schema: buildSubgraphSchema([{ typeDefs, resolvers }]),
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4002 },
});

console.log(`🚀 Posts service ready at ${url}`);

Advanced Federation Patterns

1. Entity Resolution with External Fields

# In the reviews service
extend type User @key(fields: "id") {
  id: ID! @external
  username: String! @external
  reviews: [Review!]!
  averageRating: Float! @requires(fields: "username")
}

type Review @key(fields: "id") {
  id: ID!
  rating: Int!
  comment: String!
  author: User!
  product: Product!
}

extend type Product @key(fields: "id") {
  id: ID! @external
  reviews: [Review!]!
  averageRating: Float!
}

2. Value Types and Shared Types

// Shared value types across services
const sharedTypeDefs = gql`
  scalar DateTime
  scalar JSON

  interface Node {
    id: ID!
    createdAt: DateTime!
    updatedAt: DateTime!
  }

  type Money @shareable {
    amount: Float!
    currency: String!
  }

  type Address @shareable {
    street: String!
    city: String!
    country: String!
    postalCode: String!
  }

  enum Status @shareable {
    ACTIVE
    INACTIVE
    PENDING
    ARCHIVED
  }
`;

3. Custom Directives

// Custom federation directives
const typeDefs = gql`
  directive @auth(requires: Role!) on FIELD_DEFINITION
  directive @rateLimit(max: Int!, window: String!) on FIELD_DEFINITION
  directive @deprecated(reason: String!) on FIELD_DEFINITION

  enum Role {
    USER
    ADMIN
    MODERATOR
  }

  extend type Query {
    adminUsers: [User!]! @auth(requires: ADMIN)
    userAnalytics: Analytics! @auth(requires: ADMIN) @rateLimit(max: 100, window: "1h")
  }
`;

// Implementing custom directives
const authDirective = {
  auth: (next, _, { requires }, context) => {
    if (!context.user || !context.user.roles.includes(requires)) {
      throw new Error('Unauthorized');
    }
    return next();
  },
};

Performance Optimization

1. DataLoader for N+1 Prevention

import DataLoader from 'dataloader';

// User service with DataLoader
const createUserLoader = () => new DataLoader(async (userIds) => {
  const users = await db.users.findMany({
    where: { id: { in: userIds } },
  });
  
  // Map results to match input order
  const userMap = new Map(users.map(user => [user.id, user]));
  return userIds.map(id => userMap.get(id));
});

const resolvers = {
  Post: {
    author: (post, _, context) => {
      return context.loaders.user.load(post.authorId);
    },
  },
};

// Context factory
const createContext = ({ req }) => ({
  userId: req.headers.authorization?.split(' ')[1],
  loaders: {
    user: createUserLoader(),
    post: createPostLoader(),
  },
});

2. Query Planning and Caching

// Gateway with query plan caching
import { ApolloGateway, RemoteGraphQLDataSource } from '@apollo/gateway';
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';

class AuthenticatedDataSource extends RemoteGraphQLDataSource {
  willSendRequest({ request, context }) {
    // Forward auth headers to subgraphs
    request.http.headers.set('authorization', context.authorization);
    request.http.headers.set('x-user-id', context.userId);
  }
}

const gateway = new ApolloGateway({
  supergraphSdl: await loadSupergraphSdl(),
  buildService({ url }) {
    return new AuthenticatedDataSource({ url });
  },
  queryPlannerConfig: {
    cache: new InMemoryLRUCache({
      maxSize: Math.pow(2, 20) * 100, // 100MB
      ttl: 300, // 5 minutes
    }),
  },
});

3. Field-Level Metrics

// Tracking field usage and performance
import { Plugin } from '@apollo/server';

const fieldUsagePlugin: Plugin = {
  requestDidStart() {
    return {
      willResolveField(fieldContext) {
        const start = Date.now();
        
        return (error, result) => {
          const duration = Date.now() - start;
          const { fieldName, parentType } = fieldContext.info;
          
          // Track metrics
          metrics.fieldDuration.observe(
            { field: fieldName, type: parentType.name },
            duration
          );
          
          if (error) {
            metrics.fieldErrors.inc({
              field: fieldName,
              type: parentType.name,
              error: error.message,
            });
          }
        };
      },
    };
  },
};

Security Considerations

1. Authentication and Authorization

// Centralized auth in gateway
const authPlugin = {
  requestDidStart() {
    return {
      async willSendRequest(requestContext) {
        const { request, context } = requestContext;
        
        // Verify JWT token
        const token = request.http.headers.get('authorization');
        if (token) {
          try {
            const user = await verifyToken(token);
            context.user = user;
            context.userId = user.id;
          } catch (error) {
            throw new AuthenticationError('Invalid token');
          }
        }
      },
    };
  },
};

// Field-level authorization
const resolvers = {
  User: {
    email: (user, _, context) => {
      // Only show email to self or admin
      if (context.userId === user.id || context.user?.role === 'ADMIN') {
        return user.email;
      }
      return null;
    },
  },
};

2. Rate Limiting

import { RateLimiterMemory } from 'rate-limiter-flexible';

const rateLimiter = new RateLimiterMemory({
  points: 100, // Number of requests
  duration: 60, // Per minute
});

const rateLimitPlugin = {
  requestDidStart() {
    return {
      async willSendRequest(requestContext) {
        const { context } = requestContext;
        const key = context.userId || context.ip;
        
        try {
          await rateLimiter.consume(key);
        } catch (rejRes) {
          throw new Error('Too many requests');
        }
      },
    };
  },
};

Testing Federation

1. Integration Testing

import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { createTestClient } from 'apollo-server-testing';

describe('User Service', () => {
  let server;
  let query, mutate;
  
  beforeAll(async () => {
    server = new ApolloServer({
      schema: buildSubgraphSchema([{ typeDefs, resolvers }]),
    });
    
    const testClient = createTestClient(server);
    query = testClient.query;
    mutate = testClient.mutate;
  });
  
  test('should fetch user by id', async () => {
    const GET_USER = gql`
      query GetUser($id: ID!) {
        user(id: $id) {
          id
          username
          email
        }
      }
    `;
    
    const { data } = await query({
      query: GET_USER,
      variables: { id: '1' },
    });
    
    expect(data.user).toMatchObject({
      id: '1',
      username: 'alice',
      email: '[email protected]',
    });
  });
});

2. Schema Validation

// Validate composed schema
import { composeAndValidate } from '@apollo/federation';

const serviceList = [
  {
    name: 'users',
    typeDefs: usersTypeDefs,
  },
  {
    name: 'posts',
    typeDefs: postsTypeDefs,
  },
];

const { errors, supergraphSdl } = composeAndValidate(serviceList);

if (errors && errors.length > 0) {
  throw new Error(errors.map(e => e.message).join('\n'));
}

Production Deployment

1. Docker Compose Setup

# docker-compose.yml
version: '3.8'

services:
  gateway:
    build: ./gateway
    ports:
      - "4000:4000"
    environment:
      - NODE_ENV=production
      - APOLLO_KEY=${APOLLO_KEY}
      - APOLLO_GRAPH_REF=${APOLLO_GRAPH_REF}
    depends_on:
      - users
      - posts
      - comments
    
  users:
    build: ./services/users
    environment:
      - DATABASE_URL=postgresql://user:pass@postgres:5432/users
      - REDIS_URL=redis://redis:6379
    depends_on:
      - postgres
      - redis
  
  posts:
    build: ./services/posts
    environment:
      - DATABASE_URL=postgresql://user:pass@postgres:5432/posts
      - REDIS_URL=redis://redis:6379
    depends_on:
      - postgres
      - redis
  
  postgres:
    image: postgres:15
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
    volumes:
      - postgres_data:/var/lib/postgresql/data
  
  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:

2. Kubernetes Deployment

# gateway-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: apollo-gateway
spec:
  replicas: 3
  selector:
    matchLabels:
      app: apollo-gateway
  template:
    metadata:
      labels:
        app: apollo-gateway
    spec:
      containers:
      - name: gateway
        image: myregistry/apollo-gateway:latest
        ports:
        - containerPort: 4000
        env:
        - name: APOLLO_KEY
          valueFrom:
            secretKeyRef:
              name: apollo-secrets
              key: apollo-key
        - name: SERVICE_LIST
          value: |
            [
              {"name": "users", "url": "http://users-service:4001/graphql"},
              {"name": "posts", "url": "http://posts-service:4002/graphql"}
            ]
        livenessProbe:
          httpGet:
            path: /.well-known/apollo/server-health
            port: 4000
          initialDelaySeconds: 30
          periodSeconds: 10

Monitoring and Observability

1. Apollo Studio Integration

// Enhanced gateway with Apollo Studio
import { ApolloServer } from '@apollo/server';
import { ApolloGateway } from '@apollo/gateway';
import { ApolloServerPluginUsageReporting } from '@apollo/server/plugin/usageReporting';

const server = new ApolloServer({
  gateway,
  plugins: [
    ApolloServerPluginUsageReporting({
      sendVariableValues: { all: true },
      sendHeaders: { all: true },
      generateClientInfo: ({ request }) => {
        const headers = request.http.headers;
        return {
          clientName: headers.get('apollo-client-name'),
          clientVersion: headers.get('apollo-client-version'),
        };
      },
    }),
  ],
});

2. Custom Metrics with Prometheus

import { register, Counter, Histogram } from 'prom-client';

const queryCounter = new Counter({
  name: 'graphql_queries_total',
  help: 'Total number of GraphQL queries',
  labelNames: ['operation', 'service'],
});

const queryDuration = new Histogram({
  name: 'graphql_query_duration_seconds',
  help: 'GraphQL query duration in seconds',
  labelNames: ['operation', 'service'],
  buckets: [0.1, 0.5, 1, 2, 5],
});

// Metrics endpoint
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});

Best Practices

  1. Service Boundaries: Define clear boundaries between services based on business domains
  2. Shared Nothing: Avoid sharing databases between services
  3. Versioning: Use field deprecation instead of versioning entire services
  4. Error Handling: Implement consistent error formatting across services
  5. Testing: Test both individual services and the composed schema
  6. Documentation: Keep schemas well-documented with descriptions
  7. Performance: Monitor and optimize resolver performance continuously

Conclusion

GraphQL Federation provides a powerful approach to building scalable, maintainable GraphQL APIs in a microservices architecture. By allowing teams to work independently while maintaining a unified graph, it solves many of the challenges associated with distributed systems.

The key to success with Federation is thoughtful service design, robust testing, and comprehensive monitoring. As your system grows, Federation's benefits become increasingly apparent, enabling you to scale both your technology and your teams effectively.