Skip to content

Web Development · API Design

GraphQL Federation Explained: When One Schema Stops Being Enough

A single GraphQL schema works until multiple teams own different parts of your data. Here's what federation actually solves, how Apollo Federation composes subgraphs, and when the added complexity is worth it.

Abhishek Gupta

Abhishek Gupta

6 min read

GraphQL Federation Explained: When One Schema Stops Being Enough

Sponsored

Share

A GraphQL schema is supposed to be the one place where your API’s shape lives. That works beautifully until three different backend teams all need to add fields to the same User type, and now every pull request against the schema file needs review from whichever team didn’t write it. Federation exists to fix that specific problem, and only that problem. If you don’t have it yet, you probably don’t need federation.

The problem federation solves

Picture an e-commerce platform with three backend teams: Accounts, Catalog, and Orders. Each owns its own database and its own service. The client-facing GraphQL API needs a User type with an id and email (owned by Accounts), a list of orders (owned by Orders), and each order needs product details (owned by Catalog).

In a single monolithic schema, all three teams edit the same schema.graphql file and the same resolver codebase. Every change to the User type risks breaking someone else’s resolver. Every deploy of the API server requires coordinating three teams’ code in one release. The schema file becomes the place where independent teams collide, which is the opposite of what microservices were supposed to buy you.

Federation splits that one schema into subgraphs, one per team, each independently deployable, and composes them into a single graph that the client still queries as if it were one API.

How composition actually works

Each subgraph declares which types it owns and which fields of a shared type it contributes. The @key directive tells the gateway how to identify an entity across subgraphs:

# accounts subgraph
type User @key(fields: "id") {
  id: ID!
  email: String!
  name: String!
}
# orders subgraph
type User @key(fields: "id") {
  id: ID!
  orders: [Order!]!
}

type Order {
  id: ID!
  total: Float!
  productId: ID!
}
# catalog subgraph
type Product @key(fields: "id") {
  id: ID!
  name: String!
  price: Float!
}

Both the accounts and orders subgraphs declare a User type with the same @key. The gateway composes them into one logical type:

type User {
  id: ID!
  email: String!
  name: String!
  orders: [Order!]!
}

A client query against the composed graph looks completely ordinary:

query {
  user(id: "u_123") {
    email
    orders {
      total
      product {
        name
        price
      }
    }
  }
}

Behind the gateway, this single query gets split into a query plan that hits three separate services: one call to Accounts for email, one to Orders for orders and total, and one to Catalog for product details, using the productId from the orders subgraph as the reference key. The client never knows three services were involved.

Setting it up with Apollo Federation

A minimal subgraph using Apollo Server looks like this:

import { ApolloServer } from '@apollo/server'
import { buildSubgraphSchema } from '@apollo/subgraph'
import { startStandaloneServer } from '@apollo/server/standalone'
import gql from 'graphql-tag'

const typeDefs = gql`
  type Order @key(fields: "id") {
    id: ID!
    total: Float!
    productId: ID!
  }

  extend type User @key(fields: "id") {
    id: ID! @external
    orders: [Order!]!
  }
`

const resolvers = {
  User: {
    orders: (user) => db.orders.findByUserId(user.id),
  },
}

const server = new ApolloServer({
  schema: buildSubgraphSchema({ typeDefs, resolvers }),
})

await startStandaloneServer(server, { listen: { port: 4002 } })

The @external directive marks id as a field this subgraph references but doesn’t own, it just needs it to resolve orders. The gateway (Apollo Router, or a self-hosted @apollo/gateway instance) is configured with the URLs of each subgraph and handles composition and query planning:

import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway'

const gateway = new ApolloGateway({
  supergraphSdl: new IntrospectAndCompose({
    subgraphs: [
      { name: 'accounts', url: 'http://localhost:4001/graphql' },
      { name: 'orders', url: 'http://localhost:4002/graphql' },
      { name: 'catalog', url: 'http://localhost:4003/graphql' },
    ],
  }),
})

In production, most teams generate the composed supergraph schema ahead of time as a build step, rather than having the gateway introspect subgraphs at runtime, since that removes a network dependency from the gateway’s startup path.

What you’re trading away

A single point of failure moves, it doesn’t disappear. The gateway now needs to be available for any query to resolve, and a query planner bug or misconfiguration affects the whole graph, not one team’s slice of it.

Partial degradation is a new failure mode. If the Orders subgraph goes down but Accounts and Catalog are healthy, a query for user.email still works, but any query touching orders fails or returns partial data with errors. That’s arguably better than an all-or-nothing outage, but it’s a failure mode your monitoring and client error handling need to account for, and it doesn’t exist in a monolithic schema.

Debugging requires understanding the query plan. A slow federated query isn’t necessarily a slow resolver in one place, it might be three sequential subgraph calls where two could have run in parallel, or an @requires directive forcing an extra round trip. Apollo Router exposes query plan visualization for exactly this reason, but it’s a debugging skill your team didn’t need before.

Schema composition itself can fail. If two subgraphs declare conflicting fields on the same type without proper @override or @shareable directives, composition fails at build time. That’s a good thing, it catches conflicts before deploy, but it’s a new CI step and a new class of error message your team has to learn to read.

When it’s genuinely worth it

Federation earns its complexity when the organizational problem is real: multiple teams, each with their own deploy cadence, need to independently own parts of a graph that a client still consumes as one API. If your Orders team can’t ship a schema change without waiting for review from Accounts and Catalog, and that’s happening weekly, federation removes that bottleneck cleanly.

It does not solve performance problems, and it’s not a substitute for good API design. A monolithic schema with clear module boundaries, owned by a single well-organized team, will out-perform and out-simplify a federated graph for as long as one team can reasonably own it. If you’re evaluating GraphQL against REST and tRPC for a new project, federation isn’t a factor in that decision at all, it’s a question that only comes up after you’ve already committed to GraphQL and outgrown a single schema owner.

The honest sequencing is: start with one schema, one team, one deploy. Split into federated subgraphs when the organizational pain of a shared schema is costing you more time than the operational overhead of a gateway would. Most teams reach for federation earlier than that line, because it looks like an architecture decision when it’s actually an org chart decision wearing an architecture diagram.

Frequently asked questions

What problem does GraphQL federation actually solve?
It solves ownership, not performance. When multiple teams need to contribute fields to the same GraphQL API, without one team reviewing every pull request or one deploy blocking every other team's release, federation lets each team own a subgraph independently and compose them into a single graph at a gateway.
Is GraphQL federation the same as schema stitching?
No, and the difference matters. Schema stitching, GraphQL's earlier approach to combining schemas, requires the gateway to know the internal implementation details of each underlying service. Federation, as defined by the Apollo Federation spec, lets each subgraph declare its own contribution to shared types using directives like `@key` and `@external`, and the gateway composes them without needing service-specific glue code.
Do I need Apollo specifically to do federation?
No. Apollo Federation is a specification, not just a product. Apollo Server and Apollo Router are one implementation, but GraphQL Yoga, Mercurius, and other GraphQL servers also support the federation spec, and you can run a federated graph without any Apollo-branded software in the stack.
What's the biggest downside of federation?
Operational complexity. You now have a gateway that must be up for any query to succeed, a query planner that decides how to split a client query across subgraphs, and the possibility that a single subgraph outage degrades part of the graph instead of failing cleanly. Debugging a slow query means reasoning about the query plan, not just one resolver.
How many teams or engineers justify moving to federation?
There's no fixed number, but the pattern that triggers it is consistent: multiple teams need to independently own and deploy parts of the same graph, and a single shared schema file has become a merge-conflict and review bottleneck. Teams under roughly ten engineers working on one API rarely hit this; teams with several independent backend services that all need to expose data through one client-facing graph usually do.

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored