GraphQL Federation 2.0: Building a Unified Supergraph Across Microservices



The Problem: Your GraphQL Schema Is a Monolith

You started with a single GraphQL server. It was great. One schema, one endpoint, one team. Then the org grew. Different teams own different domains — users, products, orders, inventory. Each wants to evolve their schema independently.

The naive solution: one massive GraphQL server that every team pushes to. The problems arrive quickly: merge conflicts, accidental breaking changes, deployment coupling, and a codebase that no team truly owns.

GraphQL Federation solves this by letting each team own a subgraph — their own GraphQL service with their own schema. A router (the supergraph) composes them into a single unified API for clients.

API architecture

Photo by Luke Chesser on Unsplash


Federation 2.0: What Changed

Federation 1.x had sharp edges: @key directives scattered everywhere, confusing ownership semantics, and fragile composition. Federation 2.0 (now broadly the industry standard) introduced:

  • Explicit @shareable fields — by default, object types are not shared across subgraphs; you opt in
  • @override — cleanly migrate a field from one subgraph to another
  • @interfaceObject — reference interface types across subgraphs
  • Better error messages — composition errors now point to the exact schema line
  • Progressive adoption — easier to migrate from Federation 1 incrementally

Core Concepts

Entities and the @key Directive

Entities are types that can be referenced across subgraphs. You define them with @key:

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

type Query {
  me: User
  userById(id: ID!): User
}
# orders subgraph
extend type User @key(fields: "id") {
  id: ID! @external
  orders: [Order!]!
}

type Order {
  id: ID!
  totalAmount: Float!
  status: OrderStatus!
}

enum OrderStatus {
  PENDING
  SHIPPED
  DELIVERED
}

type Query {
  order(id: ID!): Order
}

The orders subgraph extends the User type to add the orders field. When a client queries:

query {
  me {
    name
    email
    orders {
      id
      totalAmount
      status
    }
  }
}

The router automatically plans and fans out the query: fetch User from the users subgraph, then use the returned id to fetch orders from the orders subgraph. The client sees one seamless response.


Implementing a Reference Resolver

Each subgraph needs a reference resolver to handle cross-subgraph entity lookups:

// orders subgraph (Apollo Server 4)
const resolvers = {
  User: {
    // Called when the router asks "fetch orders for this user id"
    __resolveReference: async ({ id }, { dataSources }) => {
      return { id }; // We only need the id to fetch orders
    },
    orders: async ({ id }, _, { dataSources }) => {
      return dataSources.ordersDB.getOrdersByUserId(id);
    }
  },
  Query: {
    order: (_, { id }, { dataSources }) => {
      return dataSources.ordersDB.getOrderById(id);
    }
  }
};

The router calls __resolveReference in bulk (batching entity lookups via DataLoader is strongly recommended for performance).


Schema Composition with Rover CLI

Apollo Rover is the CLI for managing supergraphs:

# Install Rover
curl -sSL https://rover.apollo.dev/nix/latest | sh

# Check a subgraph schema
rover subgraph introspect http://localhost:4001/graphql

# Compose locally (requires a supergraph.yaml)
rover supergraph compose --config supergraph.yaml

# Validate a subgraph against the published supergraph
rover subgraph check my-graph \
  --schema ./users.graphql \
  --name users \
  --routing-url http://users-service/graphql

Your supergraph.yaml defines the composition:

federation_version: =2.5.0
subgraphs:
  users:
    routing_url: http://users-service/graphql
    schema:
      subgraph_url: http://users-service/graphql
  orders:
    routing_url: http://orders-service/graphql
    schema:
      subgraph_url: http://orders-service/graphql
  products:
    routing_url: http://products-service/graphql
    schema:
      subgraph_url: http://products-service/graphql

The Router: Apollo Router vs. Apollo Gateway

Apollo Router (the Rust-based router) has replaced Apollo Gateway for most production use cases:

FeatureApollo Gateway (Node.js)Apollo Router (Rust)
PerformanceModerateHigh (10x+ throughput)
Latency overhead~5ms per request<1ms per request
MemoryHigherMuch lower
Plugin supportJavaScriptRhai scripts + native Rust
ObservabilityOpenTelemetry optionalOpenTelemetry built-in
# router.yaml - basic Apollo Router config
cors:
  origins:
    - https://app.example.com

telemetry:
  exporters:
    tracing:
      otlp:
        endpoint: http://otel-collector:4317

supergraph:
  listen: 0.0.0.0:4000
  
traffic_shaping:
  all:
    deduplicate_variables: true

Shift-Left Schema Governance

Federation 2.0 enables schema governance as code. The key pattern: check schema changes in CI before merging.

# .github/workflows/schema-check.yml
name: Schema Check
on: [pull_request]
jobs:
  schema-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install Rover
        run: curl -sSL https://rover.apollo.dev/nix/latest | sh
      - name: Check Schema
        run: |
          ~/.rover/bin/rover subgraph check $ \
            --name $ \
            --schema ./schema.graphql
        env:
          APOLLO_KEY: $
          APOLLO_GRAPH_REF: my-org/my-graph@main
          SUBGRAPH_NAME: users

This check against Apollo Studio (or any compatible schema registry) will:

  • Detect breaking changes that would affect client operations
  • Catch composition errors before they reach production
  • Link schema changes to client impact data

Advanced: Directives for Cross-Cutting Concerns

Federation 2.0 lets you define custom directives that apply across the supergraph:

# Defined in one subgraph, imported by others
directive @authenticated on FIELD_DEFINITION
directive @requiresRole(role: String!) on FIELD_DEFINITION

type User @key(fields: "id") {
  id: ID!
  email: String! @authenticated
  adminData: AdminData @requiresRole(role: "ADMIN")
}

The router can intercept and enforce these via coprocessors — external HTTP services that the router calls before/after field resolution.


Observability Out of the Box

Apollo Router ships with OpenTelemetry integration:

telemetry:
  instrumentation:
    spans:
      router:
        attributes:
          "operation.name": true
          "graphql.operation.type": true
      supergraph:
        attributes:
          "graphql.document": true
      subgraph:
        attributes:
          "subgraph.name": true
          http.response.status_code: true

Every request generates spans that trace the full query plan execution — which subgraphs were called, in what order, with what latency. Correlates directly with your Tempo/Jaeger traces.


When to Use Federation

Good fit:

  • Multiple teams each owning a GraphQL domain
  • Existing REST/GraphQL services you want to unify
  • Need for independent deployability of API components
  • Large schema that’s painful to maintain as a monolith

Poor fit:

  • Single-team, single-service GraphQL server (unnecessary complexity)
  • Need for real-time subscriptions heavily across subgraphs (challenging in Federation)
  • Teams that don’t have clear domain ownership

GraphQL Federation 2.0 has matured to the point where it’s the default recommendation for any org running GraphQL at scale. The router performance improvements, cleaner composition semantics, and robust tooling make it significantly less painful than its first iteration. If you deferred Federation because v1 was rough, it’s time to revisit.

이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)