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

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.

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.