When to Use Schema Stitching vs Apollo Federation
This page answers one decision: given your team topology, traffic, and tooling, should you unify your GraphQL services with runtime schema stitching or with build-time Apollo Federation v2? Both patterns merge disparate services into a single graph, but their composition phase, routing mechanics, and failure modes diverge sharply, and choosing wrong tends to surface as cascading routing failures or unmanageable type collisions. This guide sits beside resolving schema conflicts in Apollo Federation within GraphQL Federation Architecture & Design; read the parent first if you need the conflict taxonomy.
When to use each pattern
Reach for schema stitching when:
- A single platform team controls every service, so manual resolver mapping stays tractable.
- You are wrapping REST endpoints in a lightweight GraphQL facade and cannot refactor downstream services into subgraphs.
- Query volume is low or the project is a prototype, and you have no schema-validation gates to satisfy.
Reach for Apollo Federation when:
- Multiple squads own distinct bounded contexts and deploy on independent cycles.
- You need deterministic query planning, predictable latency SLAs, and automatic entity batching.
- Your pipeline must diff schemas and block breaking changes — covered in schema validation in CI/CD pipelines.
Core architectural differences
Stitching composes at runtime: the gateway introspects remote schemas on startup and you wire shared types together with explicit resolver delegation. Federation composes at build time: a router validates every subgraph SDL into a supergraph before deployment and precomputes query plans, with distributed ownership expressed through @key directives and _entities resolution.
| Dimension | Schema Stitching | Apollo Federation |
|---|---|---|
| Composition phase | Runtime (gateway merges on startup) | Build-time (router validates before deploy) |
| Routing logic | Explicit delegateToSchema chains |
Query planner precomputes execution paths |
| Type resolution | Manual type mapping & field merging | @key + _entities for distributed ownership |
| Failure surface | Runtime resolver errors, silent field drops | Composition failures, directive mismatches, planning timeouts |
| Ownership model | Centralised at the gateway | Decentralised per subgraph |
Prerequisites
Implementation walkthrough
Minimal stitching gateway
import { stitchSchemas } from '@graphql-tools/stitch';
import { buildHTTPExecutor } from '@graphql-tools/executor-http';
import { schemaFromExecutor } from '@graphql-tools/wrap';
const aExec = buildHTTPExecutor({ endpoint: 'http://service-a/graphql' });
const bExec = buildHTTPExecutor({ endpoint: 'http://service-b/graphql' });
const gatewaySchema = stitchSchemas({
subschemas: [
{ schema: await schemaFromExecutor(aExec), executor: aExec },
{
schema: await schemaFromExecutor(bExec),
executor: bExec,
// Manual type merge: how the gateway re-fetches a User by id
merge: {
User: {
selectionSet: '{ id }',
fieldName: 'userById',
args: ({ id }: { id: string }) => ({ id }),
},
},
},
],
});
The merge block is the heart of stitching and the source of most of its pain: every shared type needs an explicit selection set, a query field to re-fetch it, and an argument mapper, all maintained by hand at the gateway.
Equivalent Federation subgraph
Federation pushes that contract into the SDL. The owning subgraph declares a @key, and any extending subgraph references the entity declaratively — no gateway-side merge config. This is the entity resolver pattern.
# product subgraph SDL
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@key"])
type Query {
product(id: ID!): Product
}
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
}
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'graphql-tag';
const typeDefs = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@key"])
type Query { product(id: ID!): Product }
type Product @key(fields: "id") { id: ID! name: String! price: Float! }
`;
const resolvers = {
Query: { product: (_: unknown, { id }: { id: string }) => db.product(id) },
// Router calls this with the @key fields during _entities resolution
Product: { __resolveReference: (ref: { id: string }) => db.product(ref.id) },
};
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers }),
});
const { url } = await startStandaloneServer(server, { listen: { port: 4001 } });
Migrating from stitching to Federation
Do not attempt a big-bang rewrite. Move incrementally:
- Map ownership. Assign each shared type a single owning subgraph and extract its
@keyfields. - Run a parallel router. Stand up an Apollo Router beside the stitching gateway and route a small slice of traffic to it via a header (
X-GraphQL-Router: federation) split at a reverse proxy. - Move delegation into subgraphs. Replace each
delegateToSchemachain andmergeblock with@key,@external, and__resolveReference. - Gate the schema. Add
rover subgraph checkto CI so breaking changes are blocked before they reach the router. - Cut over and decommission. Ramp router traffic to 100%, watching
_entitieslatency and plan-cache hit rate, then retire the gateway.
Verification steps
Confirm which model is live and that Federation composes cleanly:
# Is the endpoint federated? Look for _entities / _service in the SDL.
rover subgraph introspect http://localhost:4001/graphql | grep -E '_entities|_service'
# Compose locally and confirm no conflicts
rover supergraph compose --config supergraph.yaml --output supergraph.graphql
A successful compose prints the merged supergraph SDL and exits 0. Then issue a query that spans two subgraphs and confirm the router returns a single merged result rather than a partial response with null non-nullable fields (the classic stitching failure signature).
Common mistakes & gotchas
- Running both models permanently. A parallel gateway and router is fine as a migration bridge, but keeping both long-term doubles your routing surface and fragments client caches. Standardise on one.
- Forgetting
@externalon extended key fields. When an extending subgraph references an entity, its@keyfields must be@external, or composition fails withKEY_FIELDS_MISSING_EXTERNAL. - Assuming Federation removes all network hops. It adds
_entitiesround-trips for cross-subgraph fields; batch them with DataLoader, or you trade stitching’s N+1 for federation’s.
Frequently Asked Questions
Can I run schema stitching and Apollo Federation in the same architecture?
Technically yes, but it introduces severe routing complexity and competing query-planning logic. Treat it only as a temporary migration bridge and standardise on a single composition model once cutover is complete.
Does Apollo Federation replace every schema-stitching use case?
No. Federation excels in distributed, multi-team environments with strict CI gates. Stitching stays practical for lightweight internal APIs, REST-facade wrapping, or single-team setups that prefer manual resolver mapping.
How do I handle shared types like User across subgraphs in Federation?
One subgraph owns the base type and declares the @key; others reference it and mark the key fields @external. That single source of truth is what prevents the duplicate-definition errors detailed in resolving schema conflicts in Apollo Federation.