GraphQL Federation solves one of the fundamental tensions in microservices architecture: how do you provide a unified API to clients while keeping services independent? Without federation, teams either build monolithic GraphQL servers that couple all services together, or force clients to know about and query multiple GraphQL endpoints. Federation offers a third path where services own their schemas independently while presenting a unified graph to clients.
The core insight of federation is that a single conceptual entity often spans multiple services. A User might be defined in an identity service, have orders in an order service, and have recommendations from a personalization service. Federation lets each service contribute fields to the User type while maintaining clear ownership boundaries.
Federation Architecture
A federated graph consists of subgraphs and a router. Each subgraph is a GraphQL server owned by a team, defining the types and fields that team is responsible for. The router composes these subgraphs into a unified schema and handles query planning and execution.
When a client sends a query, the router analyzes it, determines which subgraphs can resolve which fields, and orchestrates requests to those subgraphs. The client sees a single GraphQL endpoint; the complexity of distribution is hidden.
Here's how two subgraphs might define their contributions to a shared User type. The Users subgraph owns the core identity fields, while the Orders subgraph extends User with commerce-related data.
# Users subgraph - owned by Identity team
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
createdAt: DateTime!
}
type Query {
user(id: ID!): User
me: User
}
# Orders subgraph - owned by Commerce team
type User @key(fields: "id") {
id: ID!
orders: [Order!]!
totalSpent: Money!
}
type Order @key(fields: "id") {
id: ID!
items: [OrderItem!]!
total: Money!
status: OrderStatus!
createdAt: DateTime!
}
type Query {
order(id: ID!): Order
}
The @key directive marks entity types that can be referenced across subgraphs. The Users subgraph defines User with basic identity fields. The Orders subgraph extends User with order-related fields. When a query requests user { name orders { total } }, the router fetches the user from the identity service, then uses the user's ID to fetch orders from the commerce service.
Entity Resolution
Entities are types that exist across subgraph boundaries. Each subgraph that contributes to an entity must implement a reference resolver that can fetch the entity given its key fields.
The following PHP resolver shows how the Orders subgraph handles entity resolution. When the router needs order-related User fields, it calls the __resolveReference method with just the user's ID.
// Orders subgraph resolver
class UserResolver
{
// Called when the router needs to resolve User fields from this subgraph
public function __resolveReference(array $representation): ?User
{
// $representation contains the key fields: ['id' => '123']
$userId = $representation['id'];
return $this->orderRepository->getUserOrderData($userId);
}
public function orders(User $user): array
{
return $this->orderRepository->findByUserId($user->id);
}
public function totalSpent(User $user): Money
{
return $this->orderRepository->calculateTotalSpent($user->id);
}
}
The router orchestrates these resolvers. For a query spanning subgraphs, it first fetches the entity from its origin subgraph, extracts the key fields, and passes those keys to reference resolvers in other subgraphs to fetch their contributed fields.
Query Planning
The router's query planner is where federation's magic happens. Given a query, it must determine the optimal sequence of subgraph requests to fulfill it. This involves understanding field ownership, entity keys, and data dependencies.
Consider this query that spans four different subgraphs. The router must decompose it into the minimal set of subgraph requests while respecting data dependencies.
query {
user(id: "123") {
name # From Users subgraph
email # From Users subgraph
orders { # From Orders subgraph
total
status
}
recommendations { # From Recommendations subgraph
product {
name # From Products subgraph
price # From Products subgraph
}
}
}
}
The query plan might be:
- Fetch
user(id: "123") { id name email }from Users subgraph - In parallel:
- Fetch
_entities(representations: [{__typename: "User", id: "123"}]) { orders { total status } }from Orders subgraph - Fetch
_entities(representations: [{__typename: "User", id: "123"}]) { recommendations { product { id } } }from Recommendations subgraph
- Fetch
- Fetch
_entities(representations: [{__typename: "Product", id: "..."}]) { name price }from Products subgraph for each product
Good query planners minimize round trips by parallelizing independent requests and batching entity fetches.
Schema Design for Federation
Federation works best when schemas are designed with clear ownership in mind. Each type should have a primary owner responsible for its core fields. Other services extend the type with their domain-specific fields.
Avoid circular dependencies between subgraphs. If Users depends on Orders and Orders depends on Users, query planning becomes complex and performance suffers. Design entity keys carefully; they should be stable identifiers that don't change.
The following schema demonstrates clean type extension patterns. The Products subgraph owns the core product definition, while Inventory and Reviews extend it with their domain-specific concerns.
# Good: Clear primary owner, extensions add domain-specific fields
type Product @key(fields: "id") {
# Core fields owned by Products subgraph
id: ID!
name: String!
description: String!
price: Money!
}
# Inventory subgraph extends with inventory data
extend type Product @key(fields: "id") {
id: ID! @external
inStock: Boolean!
quantity: Int!
warehouses: [Warehouse!]!
}
# Reviews subgraph extends with review data
extend type Product @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
averageRating: Float
reviewCount: Int!
}
Notice how each extension marks the key field as @external to indicate it's defined elsewhere. This clarity helps both tooling and developers understand ownership boundaries.
Performance Considerations
Federation introduces network overhead. A query that would be a single database call in a monolith becomes multiple HTTP requests to subgraphs. This overhead is usually acceptable for the organizational benefits federation provides, but it requires attention.
Batching reduces overhead by combining multiple entity fetches into single requests. Instead of fetching each product individually, the router batches all product IDs into one subgraph request.
Caching at the router level avoids redundant subgraph calls. If multiple queries request the same user within a time window, the router can cache the first response.
The DataLoader pattern is essential for efficient entity resolution. This implementation collects entity requests and executes them in a single batch, dramatically reducing database queries.
// DataLoader pattern for batching entity resolution
class ProductLoader
{
private array $pending = [];
private array $cache = [];
public function load(string $id): Promise
{
if (isset($this->cache[$id])) {
return Promise::resolve($this->cache[$id]);
}
$this->pending[$id] = $this->pending[$id] ?? new Deferred();
// Schedule batch fetch on next tick
$this->scheduleBatch();
return $this->pending[$id]->promise();
}
private function executeBatch(): void
{
$ids = array_keys($this->pending);
$products = $this->repository->findByIds($ids);
foreach ($products as $product) {
$this->cache[$product->id] = $product;
$this->pending[$product->id]->resolve($product);
unset($this->pending[$product->id]);
}
}
}
This pattern turns N+1 queries into a single batched query, which becomes critical when resolving lists of entities.
Schema Evolution
Federation simplifies schema evolution by localizing changes. When the Orders team adds a field to Order, they deploy their subgraph independently. The router detects the schema change and updates the composed schema. Other teams are unaffected.
Breaking changes require more coordination. Removing a field, changing a field's type, or modifying entity keys can break clients or other subgraphs. Federation doesn't eliminate the need for schema governance; it just localizes most changes.
Schema registries track subgraph schemas and validate composition. Before deploying a schema change, the registry checks that it composes successfully with other subgraphs and doesn't break existing contracts. This catches problems before they reach production.
When to Use Federation
Federation shines when multiple teams need to contribute to a unified API. If you have a single team managing your GraphQL layer, federation adds complexity without proportional benefit. But with multiple teams wanting to own their API surface, federation provides clear ownership boundaries.
The organizational benefit is the primary driver. Federation enables teams to deploy independently, own their schemas, and evolve at their own pace. The technical implementation serves this organizational goal.
Consider federation when your monolithic GraphQL server becomes a bottleneck for team autonomy. If changes require coordination across teams, if deployments are complex because everything is coupled together, if no one feels ownership of the schema; these are signs federation might help.
Conclusion
GraphQL Federation enables a unified graph built from independently deployable subgraphs. Each service owns its piece of the schema while clients see a cohesive API. The router handles the complexity of query planning and execution across subgraph boundaries.
Success with federation requires thoughtful schema design, clear ownership models, and attention to performance. It's not a silver bullet; it adds operational complexity that must be justified by organizational benefits. For teams that need to scale API development across multiple autonomous groups, federation provides a proven architecture.