Skip to main content
Back to Research

GraphQL Federation for Microservices at Scale

How GraphQL Federation unifies distributed microservice graphs into a single API surface — subgraph design, entity resolution, performance patterns, and…

June 8, 2026
7 min read

GraphQL Federation solves one of the hardest problems in microservice architecture: providing clients with a unified API without creating a monolithic API gateway that becomes a bottleneck and a development bottleneck. With federation, each service owns and publishes its portion of the graph. A router stitches them together at query time.

The Problem Federation Solves

In a non-federated microservice environment, a mobile client wanting to display a user's recent orders with product details must either:

  1. Call the Users service, then the Orders service, then the Products service separately — three round trips, complex client-side joins
  2. Go through a hand-written API gateway that orchestrates these calls — which every team must coordinate to modify

GraphQL Federation gives you option three: a single query that crosses service boundaries transparently, with each service team maintaining independent control over their schema slice.

Federation Architecture

A federated system has three layers:

Subgraphs — individual GraphQL services that expose their domain entities. Each subgraph is a complete, independently deployable GraphQL server.

Router — the Apollo Router (or compatible implementation) that receives client queries, plans how to split them across subgraphs, executes the plan, and merges the results.

Supergraph schema — the composed schema that combines all subgraph schemas. Built by the Apollo Rover CLI during CI/CD, not at runtime.

Client → Router (supergraph) → Users Subgraph
                             → Orders Subgraph
                             → Products Subgraph

Defining Subgraph Schemas

Each subgraph defines its entities using @key directives. The @key marks which field(s) uniquely identify an entity so other subgraphs can reference and extend it.

Users Subgraph

# users-subgraph/schema.graphql
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.3",
        import: ["@key", "@shareable"])

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

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

Orders Subgraph

The Orders subgraph references User without importing the full Users schema. It defines a "stub" of User with only the fields it needs to resolve:

# orders-subgraph/schema.graphql
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.3",
        import: ["@key", "@external", "@requires"])

type User @key(fields: "id") {
  id: ID!
  orders(limit: Int = 10, status: OrderStatus): [Order!]!
}

type Order @key(fields: "id") {
  id: ID!
  status: OrderStatus!
  totalAmountCents: Int!
  placedAt: DateTime!
  lineItems: [LineItem!]!
}

type LineItem {
  productId: ID!
  quantity: Int!
  unitPriceCents: Int!
}

enum OrderStatus {
  PENDING
  PAID
  SHIPPED
  DELIVERED
  CANCELLED
}

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

Entity Resolution

When the router needs to fetch orders for a specific user, it calls the Orders subgraph's __resolveReference function with just the user's id:

// orders-subgraph/resolvers.ts
const resolvers = {
  User: {
    __resolveReference: async (reference: { id: string }, context: Context) => {
      // reference contains only the @key fields from the other subgraph
      return { id: reference.id }
    },
    orders: async (user: { id: string }, args: OrdersArgs, context: Context) => {
      return context.orderRepository.findByUserId(user.id, {
        limit: args.limit,
        status: args.status,
      })
    },
  },
  Query: {
    order: async (_: unknown, args: { id: string }, context: Context) => {
      return context.orderRepository.findById(args.id)
    },
  },
}

Query Planning and Execution

Given this client query:

query UserDashboard($userId: ID!) {
  user(id: $userId) {
    displayName
    email
    orders(limit: 5, status: PAID) {
      id
      totalAmountCents
      placedAt
    }
  }
}

The router generates a query plan:

  1. Fetch user.displayName and user.email from Users subgraph
  2. Fetch user.orders from Orders subgraph using user.id from step 1

This is two subgraph calls, not three separate client round trips. The router executes them with the minimum number of network calls required by the dependency graph.

The N+1 Problem at Subgraph Boundaries

If a query returns a list of orders and each order needs product details from a Products subgraph, a naive implementation makes one request to Products for each order. The router batches these automatically using the @key mechanism — all product IDs are collected and sent in a single batched request.

Implement DataLoader in each subgraph to handle within-subgraph N+1 problems:

// products-subgraph/loaders.ts
export function createProductLoader(db: Database) {
  return new DataLoader<string, Product>(async (productIds) => {
    const products = await db.products.findMany({
      where: { id: { in: productIds as string[] } },
    })
    const productMap = new Map(products.map(p => [p.id, p]))
    return productIds.map(id => productMap.get(id) ?? new Error(`Product ${id} not found`))
  })
}

Schema Composition and CI/CD

The supergraph schema is composed by Rover during CI, not at router startup. This means schema incompatibilities between subgraphs are caught before deployment:

# .github/workflows/schema-check.yml
- name: Check subgraph schema compatibility
  run: |
    rover subgraph check my-graph@production \
      --name orders \
      --schema ./orders-subgraph/schema.graphql \
      --routing-url https://orders.internal/graphql

- name: Publish to Apollo Registry
  if: github.ref == 'refs/heads/main'
  run: |
    rover subgraph publish my-graph@production \
      --name orders \
      --schema ./orders-subgraph/schema.graphql \
      --routing-url https://orders.internal/graphql

The Apollo Registry stores the composed supergraph and pushes schema updates to the router via uplink — no router restart required for schema changes.

Performance at Scale

Response Caching at the Router

The Apollo Router supports full-response and partial-response caching via Redis:

# router.yaml
supergraph:
  listen: 0.0.0.0:4000

plugins:
  experimental.cache_control:
    enabled: true
    redis:
      urls: ["redis://redis.internal:6379"]
    
  experimental.response_caching:
    enabled: true
    private: false
    max_age: "30s"

Subgraphs signal cache control via HTTP headers or @cacheControl directives. Product catalog queries can cache for minutes; user-specific data should not cache at the router level.

Subgraph Performance Monitoring

Each subgraph call emits a span to your distributed tracing system. Monitor:

  • Fetch duration per subgraph — identifies slow downstream services
  • Query plan complexity — deeply nested queries requiring many sequential subgraph fetches have high latency floors
  • Entity resolution batch size — very small batches (< 5) indicate the DataLoader window is too small

Operational Patterns

Gradual Migration from Monolith

Federation excels as a migration path. Start with your existing GraphQL monolith registered as a single subgraph. Extract domains one by one:

  1. Run the monolith as subgraph legacy
  2. Build the users subgraph, move User types with @override to claim ownership
  3. Build the orders subgraph, move Order types
  4. Deprecate the legacy subgraph

The @override directive tells the router to use the new subgraph for a field, falling back to the legacy subgraph. This enables incremental migration without a flag day.

Schema Governance

At scale, schema governance prevents the federated graph from becoming inconsistent. Establish conventions enforced in CI:

  • All mutations must be namespaced: usersCreateUser, ordersPlaceOrder (not just createUser)
  • Dates must use DateTime scalar, not String
  • Pagination must follow Relay cursor spec
  • All entities must have @key(fields: "id") with a globally unique ID

A Rover custom check or a custom linter applied to every subgraph schema PR enforces these conventions automatically.

GraphQL Federation matures a microservice API strategy from "we expose many APIs" to "we expose one coherent API that happens to be implemented by many teams." The client experience becomes the first-class concern, and the service decomposition becomes an implementation detail.

Continue Reading
JCJOOTACEE / OPS

Operational laboratory for AI systems, automation infrastructures, and modular digital ecosystems.

Systems

  • AURA Orchestration
  • MCP Ecosystem
  • Graph Memory
  • AI Agents
  • Docker Infrastructure
  • Industrial Intelligence

System Status

PlatformOperational
APIHealthy
3D EngineActive
MCP Nodes8 Online

Try the Konami code...

Stay in the loop

Occasional updates on AI systems, autonomous infrastructure, and new releases.

© 2026 JootaCee. All systems operational.

RSSChangelogNext.js 16 + React 19 + R3F + GSAP