Apollo Federation lets you build a single GraphQL API from multiple independent microservices.

Here’s a breakdown of how it works and what you need to know.

The Core Idea: A Supergraph

Imagine you have several microservices, each with its own GraphQL API. One for users, one for products, one for orders. A client wants to fetch a user’s name and their most recent order details. Without Federation, the client would need to:

  1. Query the user service for the user’s name.
  2. Query the order service for the user’s orders.
  3. Potentially query the product service for details on items in those orders.

This is inefficient, leading to multiple network round trips and complex client-side logic.

Apollo Federation introduces a Gateway that acts as a single entry point. This Gateway is aware of all the microservices (called subgraphs). It queries each subgraph individually and then composes the results into a single response for the client. The Gateway builds a supergraph, which is the unified schema of all your subgraphs.

Example: A Simple User and Product Setup

Let’s say we have two subgraphs:

User Subgraph (user-service):

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

type Query {
  currentUser: User
}

Product Subgraph (product-service):

type Product {
  id: ID!
  name: String
  price: Float
}

type Query {
  topProducts: [Product]
}

The @key directive on the User type is crucial. It tells Federation that User can be uniquely identified by its id. This allows other subgraphs to reference User and request specific fields from it.

Now, we want to define a relationship: a User can have Products they’ve purchased. This doesn’t mean the user-service knows about Product details, or product-service knows about Users. Federation handles this.

Modified User Subgraph (user-service):

type User @key(fields: "id") {
  id: ID!
  name: String
  # We can now declare that a User *has* a relation to a Product
  # The actual fetching logic will be defined in a resolver
  purchasedProducts: [Product]
}

type Query {
  currentUser: User
}

Modified Product Subgraph (product-service):

type Product @key(fields: "id") { # Added @key for consistency, though not strictly needed here if only referenced
  id: ID!
  name: String
  price: Float
}

type Query {
  topProducts: [Product]
  # We can add a query to fetch a single product by ID
  product(id: ID!): Product
}

The Gateway’s job is to know that:

  • The User type’s id and name come from user-service.
  • The Product type’s id, name, and price come from product-service.
  • To resolve User.purchasedProducts, it needs to ask user-service for the User’s id, and then potentially call product-service to find products associated with that id.

The Gateway dynamically builds the supergraph schema by introspecting each subgraph. When a query comes in, it figures out which subgraphs need to be queried and how to stitch the results together.

How the Gateway Works: Composition and Resolution

  1. Schema Composition: The Gateway starts by collecting the schemas from all registered subgraphs. It merges them, resolving type conflicts and identifying relationships. The @key directive is fundamental here; it allows subgraphs to declare ownership of a type and how to fetch a specific instance of it. For example, if user-service declares type User @key(fields: "id"), and order-service has a field user: User, the Gateway knows that to resolve order.user, it needs to fetch the id from the order-service’s result and then use that id to query the user-service for the full User object. This process is called entity resolution.

  2. Query Planning: When a client sends a query, the Gateway analyzes it against the supergraph schema. It breaks down the query into sub-queries, each tailored for a specific subgraph. If a query asks for currentUser { id name purchasedProducts { name } }:

    • It knows id and name for User come from user-service.
    • It knows purchasedProducts is a field on User. To resolve this, it needs to ask the user-service to fetch the User’s id (because User is keyed by id).
    • Once it has the User’s id, it needs to figure out how to get purchasedProducts. This involves a chained query: the Gateway might ask user-service for a list of product IDs purchased by the user, and then it will ask product-service for the details of those products using their IDs.
  3. Parallel Execution: The Gateway executes these sub-queries against the respective microservices. Critically, it can execute independent sub-queries in parallel, significantly reducing latency compared to sequential calls.

  4. Response Merging: Finally, the Gateway stitches the results from all subgraphs back together into a single, unified GraphQL response that matches the client’s original query.

Key Concepts and Configuration

  • @key Directive: Declares a type and its fields that uniquely identify an entity. This is how subgraphs "own" parts of the supergraph and how relationships are federated.
  • Entity Resolution: The process by which the Gateway fetches a specific instance of a federated entity (e.g., a User with a given id) from its owning subgraph.
  • extend type: Allows subgraphs to add fields to types owned by other subgraphs. For example, user-service might extend type Product { owner: User }.
  • Gateway Configuration: You tell the Gateway which subgraphs to connect to. This is typically done by providing a list of subgraph URLs or by using a configuration file.

Example Gateway setup (using Node.js with Apollo Server):

const { ApolloServer } = require('apollo-server');
const { ApolloGateway } = require('apollo-gateway');

const gateway = new ApolloGateway({
  serviceList: [
    { name: 'users', url: 'http://localhost:4001/graphql' },
    { name: 'products', url: 'http://localhost:4002/graphql' },
  ],
});

const server = new ApolloServer({
  gateway,
  subscriptions: false, // Or configure as needed
});

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

In this setup, the Gateway at http://localhost:4000 will automatically discover the schemas from http://localhost:4001 (users) and http://localhost:4002 (products), compose them, and handle incoming client queries.

The Counterintuitive Part: Data Fetching Logic

Most people assume that if user-service defines extend type Product { owner: User }, then user-service must contain the logic to fetch the Product’s owner. This is incorrect. The owner field on Product is still primarily the responsibility of the product-service (or whatever service owns the Product type). When the Gateway needs to resolve Product.owner, it will ask the product-service to provide the owner field for a given Product. The product-service resolver for owner would then use the Product’s ID to query the user-service for the corresponding User. Federation enables inter-service communication for data resolution, but the ownership and primary resolution logic for a type typically remains with its defining service.

The next step in mastering Federation is understanding how to handle complex data relationships and cross-subgraph mutations.

Want structured learning?

Take the full Graphql-tools course →