Cross-Service Data Aggregation with Apollo Federation
Within Advanced GraphQL Federation Patterns, this guide covers aggregation. 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:
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.
// 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.
// 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.
Configuration Reference
| Setting | Value | Why |
|---|---|---|
| Composition | static supergraph from CI (rover supergraph compose) |
IntrospectAndCompose is for development only; production should not compose at startup. |
| Header propagation | tenant, auth, locale, preview (validated) | Subgraphs need request context to resolve correctly. |
| Subgraph timeout | 500 to 1,500 ms | One slow source cannot stall the whole response. |
| Query depth / cost limits | enforced at the gateway | Rejects expensive joins before planning. |
| Cache hints | @cacheControl per field |
The router computes the response policy. |
Gotchas & Edge Cases
- Runtime composition in production.
IntrospectAndComposeintrospects subgraphs at startup, so a subgraph outage during a deploy breaks the gateway, and composition errors surface in production. Compose in CI and deploy the supergraph file, or use the Apollo Router with managed federation. - Forwarding every header. Copying all incoming headers to subgraphs leaks cookies and internal headers. Forward an explicit allow-list.
- Caching across tenants. A response cache keyed without the tenant serves one tenant’s data to another. Include tenant and locale in any cache key at the gateway or CDN.
- Missing key fields in queries. An extending subgraph that does not select the key fields cannot be joined, and the gateway returns nulls. Composition catches missing
@keydefinitions, not missing selections in client fragments.
Worked Example
An electronics retailer aggregated product data from its commerce platform, editorial content from Contentful and customer-specific pricing from an identity service. The first version stitched the three APIs in the frontend, with six sequential requests on product pages. Moving to a supergraph with the gateway configuration above, batched __resolveReference loaders and per-field cache hints reduced product page data fetching to one request from the frontend and three parallel subgraph calls behind it. When Contentful had a regional slowdown a month later, product pages kept rendering with prices and stock, and descriptions fell back to a short summary stored in the commerce catalogue.
Rollout Checklist
- Compose the supergraph in CI and deploy the composed schema; never compose at runtime in production.
- Forward an explicit allow-list of headers: tenant, identity, locale and validated preview state.
- Batch every
__resolveReferencewith a per-request DataLoader. - Set per-subgraph timeouts and make fields from less reliable sources nullable.
- Include tenant and locale in every cache key at the router and CDN.
- Trace operations across subgraphs and alert on entity fetches per operation.
Frequently Asked Questions
Should I use @apollo/gateway or the Apollo Router?
The Router, a compiled binary with better performance and configuration for headers, timeouts and caching, is Apollo’s recommended runtime. The Node.js gateway remains useful for local development and for teams that need custom JavaScript hooks.
How do I aggregate a REST API that has no GraphQL?
Wrap it in a small subgraph service, just as you would a CMS, adding keys and reference resolvers. The router only ever talks to GraphQL subgraphs.
Where should business logic live?
In subgraphs, close to the data they own. The gateway should route, plan and forward context, nothing more, so it stays simple to operate and upgrade.
How do I debug a query that returns nulls for joined fields?
Run it in staging with query plan output enabled and look at the entity fetch step: if the extending subgraph receives representations without the key fields, a fragment is not selecting them; if it receives them but returns nulls, its __resolveReference cannot find the entity, often because of a type mismatch between string and numeric ids.
Can the gateway cache whole responses?
It can, keyed by operation, variables, tenant and locale, but per-field hints and entity-level purges usually give better freshness. Cache whole responses only for public, non-personalized operations.
Is federation overhead noticeable?
Query planning and one extra network hop typically add a few milliseconds per operation. Parallel subgraph fetches usually more than compensate compared with sequential client-side requests.