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
Stitching runtime merge versus Federation build-time composition On the left a gateway delegates to two services at request time; on the right a build-time composer produces a supergraph the router serves with precomputed plans. Schema Stitching (runtime) Apollo Federation (build-time) Gateway service A service B delegateToSchema per request subgraphs compose supergraph Router

The deepest difference is not the wire format but where the join logic lives. Under federation, the knowledge that a Product in one service is the same Product in another is declared in the subgraph schemas themselves, as @key directives, and the router derives the join from the composed graph. Under stitching, that same knowledge lives in gateway configuration — resolver stubs, key selectors, batch functions — written by whoever owns the gateway. Both work. But the first makes the join a property of the schema, reviewable in the service that owns the data, while the second makes it a property of one central codebase that must change every time a service does.

Who owns the knowledge that two types are the same thing Under federation the key directives live in each subgraph and the router derives the join. Under stitching the gateway holds hand written resolver stubs and key selectors, so the gateway repository changes whenever a service schema changes. Same join, two very different homes for the code Federation — join declared in the subgraphs catalog subgraph @key(fields: "id") reviews subgraph @key(fields: "id") router — derives the plan no per-type code to maintain Stitching — join written in the gateway service A unmodified schema service B unmodified schema gateway — hand-written join stubs, selectionSet, batching changes with every service Stitching's advantage is precisely that the services need no changes — which is exactly why you would choose it.

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:

  1. Map ownership. Assign each shared type a single owning subgraph and extract its @key fields.
  2. 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.
  3. Move delegation into subgraphs. Replace each delegateToSchema chain and merge block with @key, @external, and __resolveReference.
  4. Gate the schema. Add rover subgraph check to CI so breaking changes are blocked before they reach the router.
  5. Cut over and decommission. Ramp router traffic to 100%, watching _entities latency and plan-cache hit rate, then retire the gateway.

Neither choice is permanent, and the migration path runs one way: a stitched gateway can be replaced subgraph by subgraph with federated ones, because the clients see the same composed schema throughout. Going the other direction — unwinding federation into stitching — means re-implementing every entity join by hand. That asymmetry is worth weighing at the decision point: if you might end up federated eventually, starting with stitching costs you the join code twice, while starting federated costs you the schema edits once.

The migration path is one-way Moving from a stitched gateway to federation converts services one at a time behind an unchanged client contract. Moving from federation back to stitching requires re-implementing every entity join in the gateway. Cheap in one direction, expensive in the other stitched gateway joins in gateway code federated supergraph joins in subgraph SDL convert one service at a time — clients unchanged re-implement every join by hand Choose stitching when you genuinely cannot change the upstream schemas — a vendor API, a frozen legacy service — and treat it as a staging post rather than a destination.

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 @external on extended key fields. When an extending subgraph references an entity, its @key fields must be @external, or composition fails with KEY_FIELDS_MISSING_EXTERNAL.
  • Assuming Federation removes all network hops. It adds _entities round-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.

Can I run both at once?

Yes, and it is often the pragmatic answer during a transition. A federated supergraph can be stitched together with an un-federatable third-party schema at the edge, so the services you control get key-based joins while the vendor API is wrapped by hand. Keep the boundary explicit: the stitched portion should be a clearly named region of the graph, not fields scattered through your own types, or nobody will be able to tell which joins are declarative and which are code.

Which one costs more to operate at ten services?

Stitching, and the gap widens with every service. Federation’s per-service cost is a schema edit reviewed by the team that owns the data; stitching’s per-service cost is gateway code reviewed by whoever owns the gateway, and that person becomes a bottleneck for every other team. The crossover in practice is somewhere around three or four services — below that the hand-written joins are genuinely simpler to reason about.

Does stitching lose anything at query time?

Not inherently — a well-written stitched gateway batches just as well as a router. What it loses is the guarantee: composition checks cannot tell you that a stitched join is still correct after an upstream schema change, because the join lives outside the schemas being checked. You find out from a failing request instead of from a failing build.