GraphQL Federation: Scaling the Graph

October 4, 2021

GraphQL solved the over-fetching and under-fetching problems of REST. But as adoption grows, a single GraphQL server becomes a bottleneck. Federation allows multiple teams to contribute to a unified graph while maintaining ownership of their domains.

Here’s how to implement GraphQL federation.

The Single Graph Problem

What Goes Wrong

monolithic_graph_problems:
  team_coupling:
    - All changes go through one team
    - Bottleneck for schema changes
    - Coordination overhead

  deployment_coupling:
    - All resolvers in one service
    - Can't deploy independently
    - Blast radius is entire API

  ownership_unclear:
    - Who owns user.orders?
    - Schema design by committee
    - Quality inconsistent

Federation Solution

┌─────────────────────────────────────────────────────────────────┐
│                         Gateway                                  │
│              (Apollo Gateway / Router)                          │
└───────────────────────────┬─────────────────────────────────────┘
                            │
        ┌───────────────────┼───────────────────┐
        │                   │                   │
        ▼                   ▼                   ▼
   ┌─────────┐        ┌─────────┐        ┌─────────┐
   │  Users  │        │ Orders  │        │Products │
   │Subgraph │        │Subgraph │        │Subgraph │
   └─────────┘        └─────────┘        └─────────┘
        │                   │                   │
        │        Owns       │       Owns        │
        ▼                   ▼                   ▼
   User type          Order type          Product type
   User.id            Order.id            Product.id
   User.email         Order.items         Product.name
   User.profile       Order.customer      Product.price

Federation Concepts

Entities

Entities are types that can be referenced across subgraphs:

# Users subgraph
type User @key(fields: "id") {
  id: ID!
  email: String!
  name: String!
  profile: Profile
}

# Orders subgraph extends User
extend type User @key(fields: "id") {
  id: ID! @external
  orders: [Order!]!
  recentOrder: Order
}

# Reviews subgraph extends User
extend type User @key(fields: "id") {
  id: ID! @external
  reviews: [Review!]!
  averageRating: Float
}

Key Directive

# Single key
type Product @key(fields: "id") {
  id: ID!
  name: String!
}

# Multiple keys (different ways to identify)
type Product @key(fields: "id") @key(fields: "sku") {
  id: ID!
  sku: String!
  name: String!
}

# Compound key
type ProductVariant @key(fields: "productId variantId") {
  productId: ID!
  variantId: ID!
  color: String
  size: String
}

Entity Resolution

// Users subgraph resolver
const resolvers = {
  User: {
    // Entity reference resolver
    __resolveReference(user: { id: string }) {
      return userService.getById(user.id);
    },
  },
  Query: {
    user: (_, { id }) => userService.getById(id),
    me: (_, __, { userId }) => userService.getById(userId),
  },
};
// Orders subgraph resolver
const resolvers = {
  User: {
    // Extends User with orders
    orders: (user: { id: string }) => {
      return orderService.getByUserId(user.id);
    },
    recentOrder: (user: { id: string }) => {
      return orderService.getMostRecent(user.id);
    },
  },
  Order: {
    __resolveReference(order: { id: string }) {
      return orderService.getById(order.id);
    },
    customer: (order) => {
      // Return reference for gateway to resolve
      return { __typename: 'User', id: order.customerId };
    },
  },
};

Implementation

Subgraph Setup

// Users subgraph
import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'graphql-tag';

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

  type Query {
    user(id: ID!): User
    me: User
  }

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

const resolvers = {
  Query: {
    user: (_, { id }) => userService.getById(id),
    me: (_, __, context) => userService.getById(context.userId),
  },
  User: {
    __resolveReference: ({ id }) => userService.getById(id),
  },
};

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

Gateway Configuration

// Apollo Router (Rust-based, production recommended)
// router.yaml
supergraph:
  listen: 0.0.0.0:4000
  introspection: true

headers:
  all:
    request:
      - propagate:
          matching: "x-.*"
      - propagate:
          named: "authorization"

cors:
  origins:
    - https://app.example.com
  allow_credentials: true
# Supergraph composition
# supergraph.yaml
federation_version: =2.0.0
subgraphs:
  users:
    routing_url: http://users-service:4001/graphql
    schema:
      subgraph_url: http://users-service:4001/graphql
  orders:
    routing_url: http://orders-service:4002/graphql
    schema:
      subgraph_url: http://orders-service:4002/graphql
  products:
    routing_url: http://products-service:4003/graphql
    schema:
      subgraph_url: http://products-service:4003/graphql

Schema Composition

# Compose supergraph schema
rover supergraph compose --config ./supergraph.yaml > supergraph.graphql

# Or use Apollo Studio managed federation
rover subgraph publish my-graph@current \
  --name users \
  --schema ./users/schema.graphql \
  --routing-url http://users-service:4001/graphql

Query Execution

How Federation Resolves

# Client query
query GetUserWithOrders {
  user(id: "123") {
    id
    name           # Users subgraph
    email          # Users subgraph
    orders {       # Orders subgraph
      id
      total
      items {
        product {  # Products subgraph
          name
          price
        }
      }
    }
  }
}
execution_plan:
  1. Gateway receives query
  2. Query plan generated:
     - Fetch User from Users subgraph
     - Fetch orders for User from Orders subgraph
     - Fetch products for items from Products subgraph
  3. Execute in optimal order (parallel where possible)
  4. Merge results
  5. Return to client

Schema Design

Ownership Boundaries

ownership_principles:
  own_your_types:
    - Define types you're authoritative for
    - Users subgraph owns User
    - Orders subgraph owns Order

  extend_others:
    - Add fields to types you don't own
    - Orders adds User.orders
    - Reviews adds Product.reviews

  reference_entities:
    - Return references for cross-graph relationships
    - Order.customer returns User reference
    - Don't duplicate resolver logic

Shared Types

# Use @shareable for types multiple subgraphs define
type Money @shareable {
  amount: Float!
  currency: String!
}

# Or define in one place and reference
# Products defines Price
type Product @key(fields: "id") {
  id: ID!
  price: Money!
}

# Orders references it
extend type Product @key(fields: "id") {
  id: ID! @external
  price: Money! @external  # Use external from products
}

Production Considerations

Performance

performance_tips:
  batching:
    - Use DataLoader for entity resolution
    - Batch requests to same subgraph

  caching:
    - Cache entity references
    - Consider CDN caching for public data
    - Apollo Studio automatic persisted queries

  query_planning:
    - Review query plans for expensive queries
    - Optimize subgraph data access
    - Consider query complexity limits
// DataLoader for batching
const userLoader = new DataLoader(async (ids: string[]) => {
  const users = await userService.getByIds(ids);
  return ids.map(id => users.find(u => u.id === id));
});

const resolvers = {
  User: {
    __resolveReference: ({ id }, context) => {
      return context.loaders.userLoader.load(id);
    },
  },
};

Observability

tracing:
  - Trace across subgraphs
  - Include subgraph name in spans
  - Track entity resolution latency

metrics:
  - Query latency by operation
  - Subgraph latency
  - Error rates by subgraph
  - Cache hit rates

logging:
  - Operation name and variables
  - Subgraph requests
  - Error details with context

Schema Evolution

schema_evolution:
  breaking_changes:
    - Run composition on PR
    - Fail if breaking changes detected
    - Require explicit approval for breaking

  versioning:
    - Prefer additive changes
    - Deprecate before removing
    - Use @deprecated directive

  ci_cd:
    - Validate schema on commit
    - Run composition in CI
    - Deploy subgraph first, then update registry

Key Takeaways

Federation isn’t just technical—it’s organizational. Teams own their domains and can move independently while clients see a unified API.