GraphQL Federation for Microservices

Reverend Philip Jan 10, 2026 7 min read

Unify multiple GraphQL services into a single API. Learn federation architecture, entity relationships, and gateway patterns.

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:

  1. Fetch user(id: "123") { id name email } from Users subgraph
  2. 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
  3. 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.

Share this article

Related Articles

Distributed Locking Patterns

Coordinate access to shared resources across services. Implement distributed locks with Redis, ZooKeeper, and databases.

Jan 16, 2026

Need help with your project?

Let's discuss how we can help you build reliable software.