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:
- Query the user service for the user’s name.
- Query the order service for the user’s orders.
- 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
Usertype’sidandnamecome fromuser-service. - The
Producttype’sid,name, andpricecome fromproduct-service. - To resolve
User.purchasedProducts, it needs to askuser-servicefor theUser’sid, and then potentially callproduct-serviceto find products associated with thatid.
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
-
Schema Composition: The Gateway starts by collecting the schemas from all registered subgraphs. It merges them, resolving type conflicts and identifying relationships. The
@keydirective is fundamental here; it allows subgraphs to declare ownership of a type and how to fetch a specific instance of it. For example, ifuser-servicedeclarestype User @key(fields: "id"), andorder-servicehas a fielduser: User, the Gateway knows that to resolveorder.user, it needs to fetch theidfrom theorder-service’s result and then use thatidto query theuser-servicefor the fullUserobject. This process is called entity resolution. -
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
idandnameforUsercome fromuser-service. - It knows
purchasedProductsis a field onUser. To resolve this, it needs to ask theuser-serviceto fetch theUser’sid(becauseUseris keyed byid). - Once it has the
User’sid, it needs to figure out how to getpurchasedProducts. This involves a chained query: the Gateway might askuser-servicefor a list of product IDs purchased by the user, and then it will askproduct-servicefor the details of those products using their IDs.
- It knows
-
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.
-
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
@keyDirective: 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
Userwith a givenid) from its owning subgraph. extend type: Allows subgraphs to add fields to types owned by other subgraphs. For example,user-servicemightextend 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.