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.
# 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.
// 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:
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.
# 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!
}
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.
// 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]);
}
}
}
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.