Skip to content

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:

// @filename: index.js
// gateway/index.js

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:

// @filename: main.py
// services/users/index.js

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: 'alice@example.com',
    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:

// @filename: main.py
// services/posts/index.js

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

// @filename: schema.graphql
# 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

// @filename: index.js
// 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

// @filename: index.js
// 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

// @filename: main.py


// 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

// @filename: index.js
// Gateway with query plan caching

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

// @filename: index.js
// Tracking field usage and performance


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

// @filename: index.js
// 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

// @filename: index.js


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

// @filename: index.js

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: 'alice@example.com',
    })
  })
})

2. Schema Validation

// @filename: index.js
// Validate composed schema


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

// @filename: index.js
// Enhanced gateway with Apollo Studio

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

// @filename: server.js


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.

GraphQL API Query Language Microservices Architecture Distributed Systems
Share:

Continue Reading

Event-Driven Architecture with Apache Kafka: A Comprehensive Guide

Master event-driven architecture using Apache Kafka. Learn about producers, consumers, topics, partitions, Kafka Streams, and Kafka Connect. Explore real-world implementations including microservices communication, CQRS, event sourcing, and production deployment best practices.

Read article
MicroservicesArchitectureDistributed Systems

Building Microservices with gRPC and Protocol Buffers

Learn how to build high-performance microservices using gRPC and Protocol Buffers. This comprehensive guide covers service definition, implementation patterns, streaming, error handling, and deployment strategies for building robust and efficient microservice architectures.

Read article
MicroservicesArchitectureDistributed Systems