Federation and Schema Stitching
Combining multiple GraphQL services into one graph: schema stitching vs. Apollo-style Federation.
The problem: one graph, many teams
As an organization grows, a single monolithic GraphQL server owned by one team becomes a bottleneck of its own — a Users team, an Orders team, and an Inventory team each own their own domain and want to deploy independently, but clients still expect to query one graph, not one endpoint per team. This is the same organizational pressure that pushes a monolith toward microservices (covered in this app's microservices track) — applied specifically to a GraphQL schema.
Schema stitching (the older approach)
Schema stitching solves this with a gateway process that fetches each underlying service's schema, merges them into one combined schema, and relies on hand-written resolvers to wire up any relationship that crosses a service boundary:
const gatewaySchema = stitchSchemas({
subschemas: [
{ schema: usersSchema, executor: usersExecutor },
{ schema: ordersSchema, executor: ordersExecutor },
],
typeDefs: `
extend type Order {
customer: User
}
`,
resolvers: {
Order: {
customer: {
selectionSet: '{ customerId }',
resolve(order, args, context, info) {
return delegateToSchema({
schema: usersSchema,
operation: 'query',
fieldName: 'user',
args: { id: order.customerId },
context,
info,
});
},
},
},
},
});
This works, but the gateway has to know the internal implementation details of how to stitch every cross-service link by hand, and that manual delegation logic grows into its own maintenance burden as more services and relationships are added — the exact problem Federation was designed to remove.
Federation (the modern approach)
Apollo Federation (and the broader open GraphQL Federation spec it's based on) inverts the responsibility: each subgraph service declares, in its own schema, which types it owns and which of its types can be referenced across service boundaries — using directives like @key, @external, @requires, and @provides — instead of a central gateway hand-writing delegation resolvers. A separate, generic gateway (often called the router) composes the subgraphs automatically at startup by reading those directives, and at query time plans out which subgraph(s) to call for any given request, stitching the partial results together.
# users-service schema
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
# orders-service schema
type Order {
id: ID!
total: Float!
customer: User!
}
extend type User @key(fields: "id") {
id: ID! @external
orders: [Order!]!
}
The @key(fields: "id") directive marks User as an entity — a type that can be referenced by its id from another subgraph without that subgraph owning its full definition. The orders service extends User with an orders field it's responsible for, while the users service still owns User's core fields (name, email). When a client asks for an order's customer.name, the gateway resolves the order from the orders subgraph, then automatically hops to the users subgraph — using a standard _entities resolution mechanism every federated subgraph implements — to fetch the rest of User's fields, with no hand-written glue code for that specific relationship anywhere.
Federation vs. stitching, side by side
| Schema stitching | Federation | |
|---|---|---|
| Cross-service links | Hand-written gateway resolvers | Declared via directives inside each subgraph's own schema |
| Ownership | Gateway must know every subservice's internal delegation logic | Each service owns and evolves its own portion independently |
| Query planning | Manual delegation code | Automatic, computed by the gateway/router at query time |
| Tooling and maturity | Older, largely superseded | Actively maintained standard (Apollo Federation, and the open Federation spec) |
| Best fit | A small number of services, one team controlling the gateway | Many teams, many independently-deployed services |
Common mistakes
- Building one enormous, single-team-owned GraphQL schema as an organization grows past a handful of services, turning the schema itself into an organizational bottleneck.
- Hand-rolling schema stitching for a large number of services when Federation's declarative
@key/@externalmodel would need far less custom gateway code to maintain over time. - Assuming federation means clients can query individual subgraph services directly — in a federated setup, clients are expected to talk only to the gateway/router, never to a subgraph service on its own.
- Forgetting that a federated entity's fields, even though spread across services, still each need a real resolver in whichever service owns them — federation composes schemas and plans queries, it doesn't eliminate the need for actual resolver logic underneath.