Cross-service data aggregation with Apollo Federation

Apollo Federation unifies independent GraphQL subgraphs — CMS content, commerce catalog, identity — into one supergraph, so clients query a single typed endpoint instead of stitching three APIs by hand. Each service keeps ownership of its types; the gateway resolves cross-service relationships at query time. This is the pattern that removes client-side orchestration without collapsing services into a monolith.

Supergraph composition

A routing gateway delegates field resolution to subgraphs that each own specific domain types. Shared entities are linked with the @key directive, which declares a common identifier. When a client requests a cross-service relationship, the gateway resolves it in two hops: fetch the reference from the owning subgraph, then pass that reference to the extending subgraph to enrich its fields.

The supergraph topology and the two-hop entity resolution look like this:

flowchart TD
  Client["Client query"] --> GW["Routing gateway (supergraph)"]
  GW -->|"hop 1: fetch @key reference"| CMS["cms-content subgraph"]
  GW -->|"hop 2: enrich by reference"| Commerce["commerce-catalog subgraph"]
  GW --> Identity["identity-service subgraph"]
  CMS -->|"merged response"| GW
  Commerce -->|"merged response"| GW
  Identity -->|"merged response"| GW
  GW --> Client

The GraphQL specification supports the type extensions and interface implementations federation builds on, so each subgraph stays an independently deployable unit. For where this sits in a broader design, see Advanced GraphQL Federation Patterns.

Gateway configuration

The @apollo/gateway package handles composition, but you own downstream request handling — service discovery, error boundaries, and context propagation. This TypeScript config initializes the gateway with environment-driven subgraph endpoints and a custom data source that forwards tenant and auth headers to every downstream request.

TypeScript
// src/gateway/index.ts
import { ApolloGateway, IntrospectAndCompose, RemoteGraphQLDataSource } from '@apollo/gateway';
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const gateway = new ApolloGateway({
  supergraphSdl: new IntrospectAndCompose({
    subgraphs: [
      { name: 'cms-content', url: process.env.CMS_GRAPHQL_URL },
      { name: 'commerce-catalog', url: process.env.COMMERCE_GRAPHQL_URL },
      { name: 'identity-service', url: process.env.AUTH_GRAPHQL_URL },
    ],
  }),
  debug: process.env.NODE_ENV !== 'production',
  buildService({ url }) {
    return new RemoteGraphQLDataSource({
      url,
      willSendRequest({ request, context }) {
        // Propagate tenant and auth headers to downstream subgraphs
        request.http?.headers.set('x-tenant-id', context.tenantId);
        request.http?.headers.set('authorization', context.authToken);
      },
    });
  },
});

export const startServer = async () => {
  const server = new ApolloServer({
    gateway,
    introspection: process.env.NODE_ENV === 'development',
  });
  const { url } = await startStandaloneServer(server);
  console.log(`🚀 Supergraph ready at ${url}`);
};

The buildService hook runs on every downstream fetch, so routing metadata travels with the query. Wrap initialization in a health check and add circuit breakers on subgraph timeouts to stop one slow service from cascading into gateway-wide failures.

Entity resolution and the N+1 trap

The most common federation failure is misconfigured entity resolution: the gateway returns null or partial data because __resolveReference doesn’t return the expected shape, or because an extending subgraph omits a @key field. If the CMS subgraph declares type Article @key(fields: "id"), every extending subgraph must request id alongside its own fields. Missing keys fail silently and turn into N+1 explosions under load.

Batch entity fetches with DataLoader inside each subgraph resolver. The gateway already batches entity fetches when one operation requests the same entity multiple times, but resolvers must return consistent shapes and handle missing references. Enable the debug flag in staging to see entity fetch cycles and trace bottlenecks before they ship. Add a query complexity limit at the gateway to reject deeply nested joins that blow past latency budgets.

Multi-tenant context and isolation

Federation does not isolate tenant data for you. In multi-tenant deployments, the gateway extracts routing headers from each request and attaches them to downstream RemoteGraphQLDataSource calls. Skip strict header propagation and a cached response can leak across tenants.

TypeScript
// src/gateway/context.ts
import { ContextFunction } from '@apollo/server';

export const buildContext: ContextFunction = async ({ req }) => {
  const tenantId = req.headers['x-tenant-id'] as string || 'default';
  const authToken = req.headers['authorization'] as string;

  return {
    tenantId,
    authToken,
    headers: {
      'x-tenant-id': tenantId,
      'authorization': authToken,
    },
  };
};

Validate these headers in each subgraph’s own authorization middleware, not just at the gateway. Defense in depth means a gateway misroute still gets rejected downstream. Log tenant context at the gateway for audit and incident traceability. This fits the security tradeoffs you weigh during Headless CMS Architecture & Platform Selection.

Cache synchronization

Aggregating slow-changing CMS content with real-time inventory breaks naive caching. Federation caches per subgraph by default, but the gateway can cache merged responses independently and serve stale aggregations after a subgraph updates. Set TTLs per field with @cacheControl: editorial content might take a 15-minute TTL while inventory needs maxAge: 0.

Disable gateway caching on highly dynamic paths via cache: false on the ApolloServer instance, or back a shared Redis cache with invalidation hooks fired by CMS webhooks. Either way, the HTTP Cache-Control semantics govern how TTLs interact across federated boundaries. Track cache hit ratio and resolver execution time to quantify federation overhead.

Schema governance

Federation breaks down without schema governance. Align type ownership across subgraphs to avoid collisions, and run a schema registry with CI validation so breaking changes are caught before deploy. Use @shareable and @override explicitly to keep cross-service joins predictable, track resolver performance and query complexity, and treat the gateway as a routing layer — not a place to put business logic. Wire contract testing and schema diffing into the deployment pipeline.

Conclusion

Federation turns fragmented subgraphs into one query surface without giving up service autonomy. Get three things right — explicit entity keys, strict tenant context propagation, and per-field cache TTLs — and you eliminate client orchestration while keeping latency and isolation predictable.