System Design

GraphQL Federation: Building Scalable Distributed Graphs

Unlock the power of GraphQL Federation to build scalable, distributed graphs. Learn how subgraphs and gateways empower microservices architecture for large-scale applications.

Khader Vali August 16, 2026 13 min read

GraphQL Federation: Building Scalable Distributed Graphs at Scale

Welcome to Khadervali.com! As a senior engineer, I’ve seen firsthand the evolution of API design – from the early days of SOAP and REST monoliths to the dynamic world of microservices. While microservices offer undeniable benefits in terms of agility and scalability, they often introduce a new layer of complexity: how do you consistently access and combine data from dozens, or even hundreds, of independent services?

Enter GraphQL. It revolutionized client-server communication by allowing clients to request exactly what they need, reducing over-fetching and under-fetching. But even GraphQL, when implemented as a single, monolithic server, can buckle under the pressure of a large, distributed system. This is where GraphQL Federation steps in, offering an elegant and powerful solution to build a unified, scalable graph across an ocean of microservices.

In this comprehensive guide, we’ll dive deep into GraphQL Federation. We’ll explore its core concepts, architectural patterns, walk through a practical implementation with code examples, discuss its myriad benefits, and highlight the challenges you might encounter. By the end, you’ll have a solid understanding of how to leverage Federation to build robust, distributed graphs that can scale with your organization.

The Monolith vs. Microservices Conundrum

Before we dissect Federation, let’s quickly set the stage by revisiting the architectural landscape it aims to improve.

The Monolithic API: Simplicity to a Fault

In the beginning, many applications started with a monolithic API. A single codebase, a single deployment, handling all business logic and data access. This approach offers simplicity in development, deployment, and testing initially. However, as applications grow, the monolith becomes a bottleneck:

  • Slow Development Cycles: A small change requires redeploying the entire application.
  • Scalability Challenges: You have to scale the entire application even if only a small part is under heavy load.
  • Technology Lock-in: Difficult to introduce new technologies or languages.
  • Team Dependencies: Teams often step on each other’s toes in a single large codebase.

Microservices: The Distributed Promise

To combat these issues, microservices emerged as the dominant architectural pattern. Services are small, autonomous, and typically own their own data and business logic. They communicate over lightweight mechanisms (like HTTP/REST or message queues).

The benefits are substantial:

  • Independent Development & Deployment: Teams can work and deploy independently.
  • Improved Scalability: Individual services can be scaled based on demand.
  • Technology Heterogeneity: Different services can use different tech stacks.
  • Resilience: Failure in one service is less likely to bring down the entire system.

However, microservices introduce their own set of complexities, especially when it comes to data access from a client perspective:

  • Data Aggregation: A single client view often requires data from multiple services. Clients might end up making many requests to different endpoints, leading to chatty APIs and network overhead.
  • Orchestration: Clients or an API Gateway need to know which service owns what data and how to combine it.
  • Consistency: Maintaining a consistent view of data across disparate services.

The GraphQL Monolith Problem

Even when adopting GraphQL, if you build a single, large GraphQL server that directly queries all your microservices, you risk recreating a “GraphQL monolith.” This central server becomes a bottleneck, a single point of failure, and a complex codebase that nobody truly owns. It might solve the client-side data aggregation problem, but it reintroduces many of the challenges of the traditional monolith for your backend teams.

This is precisely the problem GraphQL Federation aims to solve: how to leverage the power of GraphQL for clients while maintaining the autonomy and scalability benefits of a microservices architecture on the backend.

What is GraphQL Federation? The Supergraph Concept

GraphQL Federation, primarily championed by Apollo, provides a declarative way to combine multiple independent GraphQL services (called subgraphs or federated services) into a single, unified data graph known as a supergraph. From a client’s perspective, they interact with a single GraphQL endpoint, unaware that the data they’re requesting is being sourced from various backend services.

The core idea is decentralization and ownership. Each microservice team develops and deploys its own GraphQL API, responsible for a specific domain. These individual APIs are then “federated” together by a central component, typically an Apollo Gateway or Router, which acts as the entry point for all client requests.

Key Components of a Federated Architecture

  1. Subgraphs (Federated Services):

    • These are independent, self-contained GraphQL services, each responsible for a specific domain (e.g., Products, Users, Reviews, Orders).
    • They define their own GraphQL schema, including types, queries, mutations, and resolvers.
    • Crucially, they use special Federation directives (like @key, @external, @requires, @provides) to inform the gateway how their schema relates to other subgraphs.
    • Each subgraph can be developed, deployed, and scaled independently by its respective team.
  2. Gateway (or Router/Supergraph):

    • This is the public-facing GraphQL endpoint that clients interact with.
    • It doesn’t have its own resolvers or data sources. Instead, its primary role is to combine the schemas of all registered subgraphs into a single, executable supergraph schema.
    • When a client sends a query, the gateway receives it, parses it, validates it against the supergraph schema, and then intelligently breaks it down into sub-queries.
    • These sub-queries are then sent to the appropriate subgraphs, executed (potentially in parallel), and their results are aggregated back into a single response before being sent to the client.
    • Modern gateways, like the Apollo Router (written in Rust), are highly optimized for performance, query planning, and execution.

Federation vs. Schema Stitching: A Critical Distinction

It’s important to differentiate Federation from older techniques like Schema Stitching. While both aim to combine multiple GraphQL schemas, their approaches and capabilities differ significantly:

Schema Stitching: The Centralized Approach

  • In schema stitching, a central “stitching service” explicitly defines how to combine types and fields from multiple underlying GraphQL APIs.
  • It often involves writing custom resolvers in the stitching service to fetch data from different sources and combine them.
  • The central service owns the “composition logic,” meaning if a subgraph’s schema changes, the stitching service might need updates.
  • This can lead to a centralized bottleneck, similar to the GraphQL monolith problem, as the stitching service becomes complex and tightly coupled to the underlying services.

GraphQL Federation: The Decentralized Approach

  • Federation shifts the responsibility of defining how schemas combine to the subgraphs themselves, using special directives.
  • Each subgraph declares which entities it owns (via @key) and how it extends entities owned by other subgraphs (via @extends, @external, @requires).
  • The gateway automatically builds the supergraph schema by inspecting these directives and then uses this information for intelligent query planning and execution.
  • This approach promotes strong ownership and decouples the gateway from individual subgraph implementation details, making it much more scalable and resilient for distributed teams.

In essence, Federation embraces the microservices philosophy: decentralized ownership and independent evolution. This makes it a superior choice for large-scale, distributed GraphQL architectures.

GraphQL Federation: Building Scalable Distributed Graphs
Generated Image

How GraphQL Federation Works: Architecture and Flow

Let’s delve into the mechanics of GraphQL Federation, understanding the role of each component and the journey of a typical query.

The Supergraph Schema: A Unified View

The first crucial concept is the supergraph schema. This is the complete, composed schema that clients see and query against. It’s automatically generated by the gateway by introspecting all registered subgraphs and combining their individual schemas based on Federation directives.

Consider an e-commerce platform with services for Products, Users, and Reviews. Each service would expose its own GraphQL schema:

Products Subgraph Schema:

type Product @key(fields: "id") {
  id: ID!
  name: String!
  price: Float!
  description: String
}

extend type Query {
  product(id: ID!): Product
  products: [Product!]!
}

Users Subgraph Schema:

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

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

Reviews Subgraph Schema:

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

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

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

extend type Query {
  review(id: ID!): Review
  reviewsForProduct(productId: ID!): [Review!]!
}

Notice the use of Federation directives:

  • @key(fields: "id"): This directive is fundamental. It marks a type as an “entity” that can be referenced by other subgraphs. The fields argument specifies the unique identifier(s) for that entity. For instance, the Products service declares that it owns the Product type and it can be uniquely identified by its id.

  • extend type Product @key(fields: "id"): This is how subgraphs extend types owned by other subgraphs. The Reviews service doesn’t own the Product type, but it wants to add a reviews field to it. It re-declares Product with the same @key and then adds its new fields.

  • id: ID! @external: When extending a type, any fields from the base type that are needed to resolve the extended fields must be marked as @external. This tells the gateway that this field is owned by another subgraph and needs to be fetched from there if required for resolution in the current subgraph.

  • product: Product! @provides(fields: "id"): This directive is used on a field that returns an entity. It indicates that the current subgraph can provide certain fields of that entity when it returns it. In our example, the Reviews service’s Review.product field can provide the id of the associated Product. This is crucial for the gateway to know it doesn’t need to make an extra trip to the Products service just to get the `product.id` if the Reviews service already has it.

  • @requires(fields: "price"): (Not in our simple example above, but important) This directive is used on an extended field. It specifies that certain fields from the *base* type (owned by another subgraph) are required to resolve the extended field in the current subgraph. For example, if the Reviews service had a field productRatingSummary: String that needed the Product.price to calculate something, it would be productRatingSummary: String @requires(fields: "price").

The Query Execution Flow: A Dance of Components

Let’s trace a client’s GraphQL query through the federated architecture:

1. Client Sends Query to Gateway:

A client (e.g., a web or mobile app) sends a GraphQL query to the single, public-facing endpoint of the Gateway:

query GetProductDetails($productId: ID!) {
  product(id: $productId) {
    id
    name
    price
    description
    reviews {
      id
      rating
      comment
      author {
        id
        username
      }
    }
  }
}

2. Gateway Receives & Validates Query:

The Gateway receives the query. It uses the composed supergraph schema to parse, validate, and understand the client’s request. It knows that product is a root field provided by the Products service, reviews is an extended field on Product provided by the Reviews service, and author is an extended field on Review which is linked to the Users service.

3. Query Planning (The Brain of the Gateway):

This is where the magic happens. The Gateway’s query planner analyzes the query and determines the most efficient way to fetch all the requested data from the various subgraphs. It essentially creates an execution plan, which is a directed acyclic graph (DAG) of operations. This plan outlines:

  • Which subgraphs to call.
  • In what order (sequential or parallel).
  • What specific fields to request from each subgraph.
  • How to pass data (like entity IDs) between subgraph calls.

For our example query, the plan might look like this (simplified):

  • Step 1 (Products Service): Fetch product(id: $productId) { id name price description }. The gateway needs the id of the product to then query for its reviews.
  • Step 2 (Reviews Service): Using the id obtained from Step 1, fetch reviewsForProduct(productId: $productId) { id rating comment author { id } }. Note that the Reviews service also returns the author.id, which will be used for the next step.
  • Step 3 (Users Service): For each author.id received from Step 2, fetch user(id: $authorId) { id username }. This could happen in parallel for multiple authors.

The gateway optimizes this by using “entity batching” – instead of making separate requests for each author, it can send a single batch request to the Users service if the underlying GraphQL server supports it (which Apollo Server does via the _entities query).

4. Parallel Execution of Sub-Queries:

The Gateway executes the plan. It sends individual GraphQL queries (sub-queries) to the respective subgraph endpoints. Thanks to the query planning, it can often execute independent parts of the query in parallel, significantly improving overall response times compared to serial data fetching.

The sub-queries sent to the subgraphs are standard GraphQL requests, but they might include a special _entities query that allows fetching multiple entities by their keys, which is critical for efficient entity resolution across subgraphs.

5. Data Aggregation and Response:

As results come back from the subgraphs, the Gateway intelligently aggregates them, stitching together the data based on the original query and the supergraph schema. It reconstructs the complete response object as expected by the client.

6. Client Receives Unified Response:

Finally, the Gateway sends the complete, aggregated GraphQL response back to the client, which receives a single, consistent JSON object, completely unaware of the distributed backend it just interacted with.

Diagram in Words:


Client Request
      |
      V
+---------------------+
| GraphQL Gateway     |
| (Apollo Router)     |
|                     |
| 1. Parse & Validate |
| 2. Query Plan       |
| 3. Execute Sub-queries
|      |               |
|      |               | Sub-query 1 (e.g., product details)
|      V               V
|  +----------------+  +------------------+
|  | Products       |  | Reviews          |
|  | Subgraph       |  | Subgraph         |
|  | (API 1)        |<->| (API 2)          |
|  +----------------+  +------------------+
|      ^               ^
|      | Sub-query 2 (e.g., reviews for product)
|      |               | Sub-query 3 (e.g., authors for reviews)
|      V               V
|  +----------------+
|  | Users          |
|  | Subgraph       |
|  | (API 3)        |
|  +----------------+
|      ^
|      | Aggregated Results
|      |
+---------------------+
      |
      V
Client Response

This orchestrated dance ensures that despite the underlying complexity of a microservices architecture, clients interact with a single, coherent, and highly performant GraphQL API.

GraphQL Federation: Building Scalable Distributed Graphs
Generated Image

Building a Federated GraphQL Service: A Step-by-Step Guide

Let’s get our hands dirty and build a simple federated GraphQL setup for an e-commerce scenario. We’ll create three subgraphs (Products, Reviews, Users) and an Apollo Gateway.

For this example, we’ll use Node.js with Apollo Server, but the principles apply to any language/framework supporting GraphQL Federation (e.g., Spring for Java, HotChocolate for .NET, Graphene for Python).

First, ensure you have Node.js and npm/yarn installed. Create a root directory for your project:

mkdir graphql-federation-example
cd graphql-federation-example

1. Products Subgraph

This service will manage product information.

mkdir products-service
cd products-service
npm init -y
npm install @apollo/server @apollo/subgraph @apollo/server-plugin-response-cache graphql

Create `index.js` in `products-service`:

// products-service/index.js
const { ApolloServer } = require('@apollo/server');
const { buildSubgraphSchema } = require('@apollo/subgraph');
const { readFileSync } = require('fs');
const { startStandaloneServer } = require('@apollo/server/standalone');

const typeDefs = readFileSync('./schema.graphql', { encoding: 'utf-8' });

const products = [
{ id: '1', name: 'Wireless Headphones', price: 99.99, description: 'High-quality sound with noise cancellation.' },
{ id: '2', name: 'Smartwatch', price: 199.99, description: 'Track your fitness and receive notifications.' },
{ id: '3', name: 'Mechanical Keyboard', price: 129.99, description: 'Tactile feel for superior typing experience.' },
];

const resolvers = {
Query: {
product: (_, { id }) => products.find(p => p.id

Written by

Khader Vali

Senior Software Engineer specializing in cloud architecture, real-time systems, and enterprise-scale applications.

Share this article

Related Articles

GraphQL Federation: Building Scalable Distributed Graphs

Jul 16, 2026 · 12 min read

Building Feature Flag Systems for Continuous Delivery

Aug 14, 2026 · 16 min read

Full Stack TypeScript: tRPC & Prisma End-to-End

May 26, 2026 · 10 min read