GraphQL Federation allows multiple teams to build parts of a unified GraphQL API independently. After implementing federated graphs serving millions of requests daily, I’ll share patterns that work at scale and pitfalls to avoid.

The Problem with Monolithic GraphQL

Traditional GraphQL APIs become bottlenecks as organizations scale:

# Monolithic schema - single team owns everything
type Query {
  user(id: ID!): User
  product(id: ID!): Product
  order(id: ID!): Order
  review(id: ID!): Review
  # 100+ more types and fields...
}

# All resolvers in one codebase
# One deployment
# One team to review all changes
# Scaling nightmare

GraphQL Federation Solution

Federation splits the graph across services:

Client → Apollo Gateway → User Service (owns User type)
                      → Product Service (owns Product type)
                      → Order Service (owns Order type)
                      → Review Service (owns Review type)

Each service owns part of the schema and can deploy independently.

Implementing Federation

Service 1: Users Service

// users-service/schema.ts
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'apollo-server';

const typeDefs = gql`
  # Extend Query type
  extend type Query {
    user(id: ID!): User
    me: User
  }

  # Define User entity with @key directive
  type User @key(fields: "id") {
    id: ID!
    email: String!
    name: String!
    createdAt: DateTime!
  }
`;

const resolvers = {
  Query: {
    user: async (_: any, { id }: { id: string }) => {
      return await userRepository.findById(id);
    },
    me: async (_: any, __: any, context: any) => {
      return await userRepository.findById(context.userId);
    }
  },

  User: {
    // Reference resolver - allows other services to extend User
    __resolveReference: async (user: { id: string }) => {
      return await userRepository.findById(user.id);
    }
  }
};

export const schema = buildSubgraphSchema({ typeDefs, resolvers });

Service 2: Products Service

// products-service/schema.ts
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'apollo-server';

const typeDefs = gql`
  extend type Query {
    product(id: ID!): Product
    products(category: String): [Product!]!
  }

  # Extend User type from Users service
  extend type User @key(fields: "id") {
    id: ID! @external
    # Add product-specific fields to User
    purchaseHistory: [Product!]!
  }

  type Product @key(fields: "id") {
    id: ID!
    name: String!
    price: Float!
    category: String!
    # Reference to User (owned by Users service)
    seller: User!
  }
`;

const resolvers = {
  Query: {
    product: async (_: any, { id }: { id: string }) => {
      return await productRepository.findById(id);
    },
    products: async (_: any, { category }: { category?: string }) => {
      return await productRepository.findByCategory(category);
    }
  },

  Product: {
    seller: (product: any) => {
      // Return reference to User
      // Gateway will resolve using Users service
      return { __typename: 'User', id: product.sellerId };
    },
    __resolveReference: async (product: { id: string }) => {
      return await productRepository.findById(product.id);
    }
  },

  User: {
    // Extend User type with product data
    purchaseHistory: async (user: { id: string }) => {
      const orderIds = await orderRepository.findByUserId(user.id);
      return await productRepository.findByOrderIds(orderIds);
    }
  }
};

export const schema = buildSubgraphSchema({ typeDefs, resolvers });

Service 3: Reviews Service

// reviews-service/schema.ts
const typeDefs = gql`
  extend type Query {
    review(id: ID!): Review
  }

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

  # Extend User with reviews
  extend type User @key(fields: "id") {
    id: ID! @external
    reviews: [Review!]!
  }

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

const resolvers = {
  Product: {
    reviews: async (product: { id: string }) => {
      return await reviewRepository.findByProductId(product.id);
    },
    averageRating: async (product: { id: string }) => {
      return await reviewRepository.calculateAverageRating(product.id);
    }
  },

  User: {
    reviews: async (user: { id: string }) => {
      return await reviewRepository.findByAuthorId(user.id);
    }
  },

  Review: {
    product: (review: any) => {
      return { __typename: 'Product', id: review.productId };
    },
    author: (review: any) => {
      return { __typename: 'User', id: review.authorId };
    },
    __resolveReference: async (review: { id: string }) => {
      return await reviewRepository.findById(review.id);
    }
  }
};

export const schema = buildSubgraphSchema({ typeDefs, resolvers });

Gateway Configuration

// gateway/index.ts
import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
import { ApolloServer } from 'apollo-server';

const gateway = new ApolloGateway({
  supergraphSdl: new IntrospectAndCompose({
    subgraphs: [
      { name: 'users', url: 'http://users-service:4001/graphql' },
      { name: 'products', url: 'http://products-service:4002/graphql' },
      { name: 'reviews', url: 'http://reviews-service:4003/graphql' },
    ],
    // Poll for schema updates
    pollIntervalInMs: 10000,
  }),
});

const server = new ApolloServer({
  gateway,
  // Disable GraphQL Playground in production
  introspection: process.env.NODE_ENV !== 'production',
  context: ({ req }) => {
    // Extract auth token and add to context
    const token = req.headers.authorization || '';
    const userId = verifyToken(token);
    return { userId };
  },
});

server.listen({ port: 4000 }).then(({ url }) => {
  console.log(`🚀 Gateway ready at ${url}`);
});

Advanced Patterns

DataLoader for N+1 Prevention

import DataLoader from 'dataloader';

class UserService {
  private userLoader: DataLoader<string, User>;

  constructor() {
    this.userLoader = new DataLoader(async (userIds: string[]) => {
      // Batch load users
      const users = await userRepository.findByIds(userIds);

      // Return in same order as requested
      const userMap = new Map(users.map(u => [u.id, u]));
      return userIds.map(id => userMap.get(id) || null);
    });
  }

  async getUser(id: string): Promise<User | null> {
    return this.userLoader.load(id);
  }
}

// In resolver
const resolvers = {
  Product: {
    seller: async (product: any, _: any, context: any) => {
      // Uses DataLoader - batches multiple seller lookups
      return await context.userService.getUser(product.sellerId);
    }
  }
};

Computed Fields with @requires

// Extend Product with computed field that needs seller data
const typeDefs = gql`
  extend type Product @key(fields: "id") {
    id: ID! @external
    price: Float! @external
    seller: User! @external @requires(fields: "seller { name }")
    displayName: String! @requires(fields: "name seller { name }")
  }

  extend type User @key(fields: "id") {
    id: ID! @external
    name: String! @external
  }
`;

const resolvers = {
  Product: {
    displayName: (product: any) => {
      // Has access to product.name and product.seller.name
      return `${product.name} by ${product.seller.name}`;
    }
  }
};

Authorization Across Services

// Centralized authorization directives
const typeDefs = gql`
  directive @auth(requires: Role!) on FIELD_DEFINITION

  enum Role {
    USER
    ADMIN
    SELLER
  }

  extend type Query {
    adminUsers: [User!]! @auth(requires: ADMIN)
  }

  extend type Product @key(fields: "id") {
    id: ID! @external
    # Only seller can see sales data
    salesData: SalesData! @auth(requires: SELLER)
  }
`;

// Gateway-level authorization
class AuthDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field: GraphQLField<any, any>) {
    const { requires } = this.args;
    const { resolve = defaultFieldResolver } = field;

    field.resolve = async function (source, args, context, info) {
      if (!context.user) {
        throw new AuthenticationError('Not authenticated');
      }

      if (!context.user.roles.includes(requires)) {
        throw new ForbiddenError('Insufficient permissions');
      }

      return resolve.call(this, source, args, context, info);
    };
  }
}

Caching Strategies

import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';
import responseCachePlugin from 'apollo-server-plugin-response-cache';

const server = new ApolloServer({
  gateway,
  cache: new InMemoryLRUCache({
    maxSize: 100 * 1024 * 1024, // 100 MB
    ttl: 300, // 5 minutes
  }),
  plugins: [
    responseCachePlugin({
      sessionId: (context) => context.userId || null,
    }),
  ],
});

// In subgraph schemas
const typeDefs = gql`
  type Product @key(fields: "id") {
    id: ID!
    # Cache for 1 hour
    name: String! @cacheControl(maxAge: 3600)
    # Don't cache (frequently changing)
    inventory: Int! @cacheControl(maxAge: 0)
  }
`;

Error Handling

// Custom error handling in gateway
const gateway = new ApolloGateway({
  serviceHealthCheck: true,
  buildService: ({ url }) => {
    return new RemoteGraphQLDataSource({
      url,
      willSendRequest: ({ request, context }) => {
        // Forward auth headers
        request.http.headers.set('authorization',
          context.authToken || '');
      },
      didReceiveResponse: async ({ response, request, context }) => {
        // Log errors from subgraphs
        if (response.errors) {
          logger.error('Subgraph error', {
            url,
            query: request.query,
            errors: response.errors,
          });
        }
        return response;
      },
      didEncounterError: (error) => {
        // Service is down
        logger.error('Subgraph unavailable', { url, error });
      },
    });
  },
});

// Graceful degradation
const resolvers = {
  Query: {
    user: async (_: any, { id }: { id: string }) => {
      try {
        return await userRepository.findById(id);
      } catch (error) {
        logger.error('User lookup failed', { id, error });
        // Return partial data instead of complete failure
        return {
          id,
          email: 'unavailable@example.com',
          name: 'User data temporarily unavailable',
        };
      }
    }
  }
};

Performance Monitoring

import { ApolloServerPluginUsageReporting } from 'apollo-server-core';

const server = new ApolloServer({
  gateway,
  plugins: [
    // Apollo Studio integration
    ApolloServerPluginUsageReporting({
      sendVariableValues: { all: true },
      sendHeaders: { all: true },
    }),

    // Custom metrics
    {
      requestDidStart: () => ({
        async willSendResponse({ metrics, context }) {
          // Record query duration by operation
          const duration = metrics.responseCachingMetrics.responseCacheDuration;

          metricsCollector.histogram('graphql.query.duration', duration, {
            operation: context.operationName,
          });

          // Track subgraph calls
          metrics.queryPlanTrace?.forEach((trace: any) => {
            metricsCollector.counter('graphql.subgraph.calls', {
              service: trace.serviceName,
            });
          });
        },

        async didEncounterErrors({ errors }) {
          errors.forEach((error) => {
            metricsCollector.counter('graphql.errors', {
              type: error.extensions?.code || 'UNKNOWN',
            });
          });
        }
      })
    }
  ]
});

Schema Management

# Using Rover CLI for schema management

# Check if schema changes are valid
rover subgraph check my-graph@production \
  --name products \
  --schema ./products-service/schema.graphql

# Publish schema to Apollo Studio
rover subgraph publish my-graph@production \
  --name products \
  --schema ./products-service/schema.graphql \
  --routing-url http://products-service:4002/graphql

# Download supergraph schema
rover supergraph fetch my-graph@production > supergraph.graphql

Testing Federated Services

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

describe('Products Service', () => {
  let server: ApolloServer;

  beforeAll(() => {
    server = new ApolloServer({
      schema: buildSubgraphSchema({ typeDefs, resolvers }),
    });
  });

  it('resolves product by ID', async () => {
    const result = await server.executeOperation({
      query: `
        query GetProduct($id: ID!) {
          product(id: $id) {
            id
            name
            price
          }
        }
      `,
      variables: { id: '123' },
    });

    expect(result.errors).toBeUndefined();
    expect(result.data?.product).toMatchObject({
      id: '123',
      name: 'Test Product',
      price: 29.99,
    });
  });

  it('extends User type correctly', async () => {
    const result = await server.executeOperation({
      query: `
        query {
          _entities(representations: [{__typename: "User", id: "user-1"}]) {
            ... on User {
              purchaseHistory {
                id
                name
              }
            }
          }
        }
      `,
    });

    expect(result.errors).toBeUndefined();
  });
});

Deployment Strategy

# Kubernetes deployment for federated services

# Gateway
apiVersion: apps/v1
kind: Deployment
metadata:
  name: graphql-gateway
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: gateway
        image: my-org/graphql-gateway:latest
        env:
        - name: APOLLO_GRAPH_REF
          value: "my-graph@production"
        - name: APOLLO_KEY
          valueFrom:
            secretKeyRef:
              name: apollo-secrets
              key: api-key
        resources:
          requests:
            memory: "256Mi"
            cpu: "500m"
          limits:
            memory: "512Mi"
            cpu: "1000m"

---
# Subgraph service
apiVersion: apps/v1
kind: Deployment
metadata:
  name: products-service
spec:
  replicas: 2
  template:
    spec:
      containers:
      - name: products
        image: my-org/products-service:latest
        livenessProbe:
          httpGet:
            path: /.well-known/apollo/server-health
            port: 4002
        readinessProbe:
          httpGet:
            path: /.well-known/apollo/server-health
            port: 4002

Key Takeaways

  1. Use @key directive: Defines entities that can be referenced across services
  2. Prevent N+1 queries: Always use DataLoader in resolvers
  3. Plan schema ownership: Clear boundaries prevent conflicts
  4. Monitor query performance: Track which fields are slow
  5. Version schemas carefully: Breaking changes impact multiple teams
  6. Cache strategically: Use @cacheControl directive
  7. Test reference resolvers: Ensure cross-service references work

GraphQL Federation enables true microservices independence while maintaining a unified API. Start with 2-3 services, establish patterns, then scale to dozens of services as teams grow.