Splitting One Graph Across Teams: Apollo Federation v2 with @key, @requires, and @provides
Apollo Federation v2 lets each team own a subgraph while clients query one graph. Here's how @key, @requires, and @provides coordinate it, with a verifiable example.
19 Apr 2026, 00:04 UTC

Your monolithic GraphQL server worked fine with three developers. Now there are four teams, and every schema change means a merge conflict, a deploy queue, and a Slack argument about who owns the Product type. The useful takeaway: Apollo Federation v2 lets each team ship its own GraphQL service (a subgraph) while clients still see one unified graph, and three directives — @key, @requires, and @provides — do most of the coordination work.
The problem federation actually solves
The naive fix for a growing graph is schema stitching: write a gateway that manually merges schemas and delegates fields. That works, but you end up hand-writing the glue code that decides which service answers which part of a query. Federation flips this around. Each subgraph declares what it owns and what it needs, and the gateway's query planner figures out execution automatically. You trade imperative gateway code for declarative schema annotations.
Entities and @key: shared types without duplication
An entity is a type that can be resolved by more than one subgraph. You mark it with @key, naming the field (or fields) that uniquely identify it:
# Products subgraph
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
}Another subgraph can then extend that entity — it declares a stub of the same type with the same key, marks the fields it doesn't own as external, and adds its own fields:
# Reviews subgraph
type Product @key(fields: "id") {
id: ID!
reviews: [Review!]!
}
type Review {
body: String!
rating: Int!
}At runtime, when a query needs reviews for a product the Products subgraph returned, the gateway sends an entity resolution request to the Reviews subgraph using the id. Neither service knows the other exists. This is the core win: the Product type lives in two schemas, but there's one logical graph and no copy-pasted resolvers.
@requires and @provides: declaring data dependencies
Sometimes a field in one subgraph needs data owned by another. Say the Reviews subgraph computes a priceAtReview or the Inventory subgraph computes shipping cost from a product's weight, which Products owns. @requires declares that dependency:
# Inventory subgraph
type Product @key(fields: "id") {
id: ID!
weight: Float @external
shippingEstimate: Float @requires(fields: "weight")
}The planner now knows: before calling the Inventory subgraph for shippingEstimate, it must first fetch weight from Products and pass it along with the entity key. @provides is the mirror image on the way out: it tells the gateway that when a subgraph already fetches an entity, it can also return certain fields of that entity for free, so the planner can skip a second hop. Used well, @provides trims network round trips; used carelessly, it just makes plans harder to read, so apply it only where a resolver genuinely loads the data anyway.
A worked example you can verify
Assume Apollo Server v4 (or later) with the federation library on every subgraph, and Apollo Gateway v2. The steps:
- Create the Products subgraph with the
Product @key(fields: "id")schema above and a__resolveReferenceresolver that loads a product by id. - Create the Reviews subgraph extending
Productwithreviews, plus its own__resolveReference. - Start the gateway with the two subgraph URLs in its
supergraphSdlor service list configuration. Composition happens at startup; incompatible schemas fail fast here, before any traffic. - Run this query against the gateway (no special permissions needed beyond whatever auth you have):
query {
product(id: "p-1") {
id
name
reviews { body rating }
}
}The expected result is a single JSON response where name came from Products and reviews from Reviews. To confirm the federation actually happened — rather than one subgraph secretly doing everything — enable gateway tracing or inspect the query plan in Apollo Studio. You should see two fetch nodes: one to Products for the root fields, one to Reviews via entity resolution on id. If the plan shows a single fetch, something is duplicated that shouldn't be.
Trade-offs worth knowing before you commit
Federation is not free complexity-wise. Three limitations matter most:
- Key design is permanent-ish. A key must uniquely identify an entity across every subgraph, forever. Choosing a key that can collide or change (an email address, say) leads to wrong data merges or duplicate fetches, and migrating keys later touches every subgraph.
- Debugging gets distributed. A slow or wrong field no longer means one resolver in one codebase. You need tracing wired up from day one, or you'll be guessing which of five services returned the bad value.
- Version alignment. All subgraphs and the gateway need compatible federation versions (Apollo Server v4-era subgraphs with a v2 gateway). Mixing older federation v1 subgraphs can surface composition errors that only appear at gateway startup.
There's also a schema-governance shift: because any subgraph can add fields, you need composition checks in CI (rover or Studio checks) so one team's change can't break the published graph.
Where to start
Don't federate everything at once. Pick one type that's already causing cross-team friction — usually something like User or Product — give it a stable key, and split off exactly one subgraph. Verify with the trace-based check above, get comfortable reading query plans, and only then move more types over. The declarative model pays off precisely when ownership boundaries are real; if one team still owns all the code, a well-modularized monolith is simpler and honest about it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.