GraphQL Federated Entity Pattern
Share an entity across Apollo Federation subgraphs. Use @key, @external, and @shareable so each service owns the fields it knows best.
In Apollo Federation, an entity is an object type that several subgraphs can contribute to at the same time. One service owns the base fields, others add their own, and the gateway merges everything into a single GraphQL type.
I’ve used this pattern in production with 8 subgraphs and it’s the cleanest way to split a graph across teams without a monolithic schema. The alternative — schema stitching — works, but it pushes too much glue logic into the gateway. Federation keeps each subgraph focused on the data it already manages.
The pattern keeps each subgraph focused on the data it already manages — a users service owns
User.name, an orders service adds User.orders, and a reviews service adds User.reviews.
Clients query one schema, but the gateway routes each field to the right service. For a deeper
dive, see the complete guide to GraphQL Federation.
When to Use
- Different microservices own different fields of the same domain object.
- You want a unified GraphQL API without building a monolithic schema.
- Schema stitching feels too manual or error-prone for type merging.
- Teams need to deploy and own their part of the graph independently.
When NOT to Use
- The entity is only read or written by one service. Skip federation — a regular GraphQL schema is simpler and avoids the overhead.
- Subgraphs are tightly coupled and share the same database. Federation adds complexity you don’t need.
- You’re still on a GraphQL server that doesn’t support the Apollo Federation spec.
- You need connection-based pagination across federated fields — it works, but the query plans get expensive fast.
Solution
Subgraph A: User Service (owns the entity)
The base subgraph declares the entity with @key and adds __resolveReference so the gateway
can fetch it by its key.
import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { startStandaloneServer } from '@apollo/server/standalone';
const typeDefs = `#graphql
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
`;
const resolvers = {
User: {
__resolveReference(user: { id: string }) {
return userService.getById(user.id);
},
},
Query: {
user(_: unknown, { id }: { id: string }) {
return userService.getById(id);
},
},
};
const server = new ApolloServer({
schema: buildSubgraphSchema([{ typeDefs, resolvers }]),
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4001 },
});
console.log(`Users service ready at ${url}`);
Subgraph B: Order Service (extends the entity)
The order subgraph doesn’t own User, but it adds User.orders. The id field is marked
@external because another service owns it. resolvable: false tells the gateway that this
subgraph can’t fetch a User by key on its own.
const typeDefs = `#graphql
type Order @key(fields: "id") {
id: ID!
userId: ID!
total: Float!
}
type User @key(fields: "id", resolvable: false) {
id: ID! @external
orders: [Order!]!
}
`;
const resolvers = {
User: {
orders(user: { id: string }) {
return orderService.getByUserId(user.id);
},
},
Query: {
order(_: unknown, { id }: { id: string }) {
return orderService.getById(id);
},
},
};
Subgraph C: Review Service (extends the entity again)
Another subgraph can also extend User. Each subgraph only declares the fields it knows.
const typeDefs = `#graphql
type Review @key(fields: "id") {
id: ID!
productId: ID!
rating: Int!
comment: String
}
type User @key(fields: "id", resolvable: false) {
id: ID! @external
reviews: [Review!]!
}
`;
const resolvers = {
User: {
reviews(user: { id: string }) {
return reviewService.getByUserId(user.id);
},
},
};
Gateway
The gateway asks each subgraph for its schema, builds a supergraph, and routes fields automatically. For a complete setup guide with Docker, see the federation gateway setup recipe.
import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://localhost:4001/graphql' },
{ name: 'orders', url: 'http://localhost:4002/graphql' },
{ name: 'reviews', url: 'http://localhost:4003/graphql' },
],
}),
});
const server = new ApolloServer({ gateway });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`Gateway ready at ${url}`);
Client Query
The client sees a single User type, even though the fields come from three services.
query GetUserWithOrdersAndReviews {
user(id: "123") {
id
name
email
orders {
id
total
}
reviews {
id
rating
comment
}
}
}
Explanation
Federation resolves that query in three steps:
- The gateway asks the users subgraph for
user(id: "123")and the base fieldsid,name,email. - It sends the
idto the orders and reviews subgraphs through the_entitiesquery, asking forordersandreviews. - It merges the results into one
Userobject and returns it to the client.
The key pieces are:
@key(fields: "id")— declares how the gateway identifies the entity across subgraphs. Think of it as the primary key for federation routing.@external— marks a field that this subgraph uses but doesn’t own.resolvable: false— signals that this subgraph can’t resolve the entity by key on its own; it only contributes fields when another subgraph supplies the key.__resolveReference— the resolver the gateway invokes to fetch an entity by its key. You implement this on the owning subgraph; extending subgraphs don’t need it.
Federation 1 vs Federation 2
Apollo Federation 2 (released 2022) simplified several rules. The most important changes from Federation 1:
- No
@extendsneeded: In Federation 1, extending an entity requiredextend type User. Federation 2 infers it from@key+@external. The code in this pattern works with both Federation 1 and 2. @shareableis opt-in for Federation 1, default in Federation 2 for fields defined in two or more subgraphs. In Federation 2, you mark non-shareable fields with@inaccessibleinstead.- Single-field keys: Federation 2 allows subgraphs to resolve entities using only a subset of the key fields, which wasn’t possible in Federation 1.
If you’re starting a new project today, Federation 2 is the way to go. The syntax is cleaner and the tooling (Rover, Apollo Studio) defaults to it.
Debugging federation query plans
The most common production issue with federation is the N+1 problem in __resolveReference. The
gateway calls it once per entity reference, so a query that returns 100 users with orders triggers
100 separate calls to the orders subgraph. I once debugged a query plan that made 47 subgraph calls
because someone forgot @external on the id field — the gateway couldn’t batch the references.
To diagnose query plans:
- Apollo Studio — the query plan viewer shows the exact sequence of subgraph calls. Look for
repeated
_entitiescalls that could be batched. rover supergraph compose— validates schema composition locally before deploying. Catches@external/@keymismatches early.- Gateway logs — enable
debug: truein the gateway config to log query plans to stdout. - Datadog / OpenTelemetry — trace the
_entitiesspan to see how many times each subgraph is called per query.
Migrating fields with @override
Federation 2 introduced @override to move a field from one subgraph to another without downtime:
type User @key(fields: "id") {
id: ID!
# Old subgraph still resolves this field
displayName: String! @override(from: "users")
}
The new subgraph takes over resolution gradually. Once all traffic uses the new resolver, remove
the field from the old subgraph. I’ve used this to migrate a User.avatarUrl field from a legacy
service to a new media service without a single deploy window — the gateway routes to whichever
subgraph is active.
Variants
| Variant | Directive | Use Case |
|---|---|---|
| Single key | @key(fields: "id") | Most entities with a primary ID |
| Composite key | @key(fields: "tenantId id") | Multi-tenant or partitioned entities |
| Several lookup keys | @key(fields: "id") @key(fields: "email") | Lookup by more than one identifier |
| Computed field | @requires(fields: "address") | A field that needs data from another subgraph first |
| Shared field | @shareable | A field resolved by more than one subgraph |
Composite key example
type User @key(fields: "tenantId id") {
tenantId: ID!
id: ID!
name: String!
}
@requires example
The shipping subgraph computes shippingCost from the user’s address, which the users subgraph
owns.
type User @key(fields: "id", resolvable: false) {
id: ID! @external
address: String! @external
shippingCost: Float! @requires(fields: "address")
}
The gateway fetches address first, then passes it to the shipping resolver.
Best Practices
- Keep one base definition per entity. Other subgraphs add fields, but only one service owns the core type.
- Batch calls in
__resolveReference. The gateway calls it once per entity reference, so a separate query per entity won’t scale — batch them. - Mark key fields as
@externalwhen the subgraph doesn’t own them. Forgetting this causes a schema validation error. - Add new keys before removing old ones. Changing a key mid-flight breaks existing query plans that clients may have cached.
- Use Apollo Studio or the gateway’s query plan viewer to verify that fields route through the right subgraphs.
Common Mistakes
- Missing
__resolveReferenceon the owning subgraph. Without this resolver, the gateway has no way to fetch the entity by key — queries that touch the base type will fail. - Forgetting
@externalon theidfield in an extension. The gateway will reject the schema. - Declaring the base
Usertype in two subgraphs. Only one subgraph should own the base definition. - Returning extra fields in
__resolveReference. Only return what the resolver was asked for; the gateway fetches the rest from other subgraphs. - Deep circular references without query limits.
User.orders.user.orderscan blow up query plans — add depth limits or pagination to prevent it.
See Also
- Apollo Federation docs — official documentation
- Apollo Federation directives spec — @key, @external, @requires, @shareable, @override
- Apollo Studio query plan viewer — debug subgraph routing
- Rover CLI — schema composition and publishing
- GraphQL Federation 2 announcement — what changed from Federation 1
- Schema stitching pattern — alternative approach to schema merging
- Federation gateway setup — practical gateway configuration
Frequently Asked Questions
How is federation different from schema stitching?
Federation uses directives and the _entities protocol to merge schemas declaratively. Schema
stitching writes custom resolver logic in the gateway to delegate fields manually. Federation is
cleaner for Apollo services; stitching is more flexible for integrating external APIs.
Can a subgraph extend an entity it doesn't own?
Yes. It declares the entity with @key and marks the key field as @external. The gateway routes
base fields to the owning subgraph and the new fields to the extending subgraph.
What does @requires do?
It tells the gateway to fetch one or more fields from another subgraph before resolving the annotated field. Use it when a field is computed from data owned elsewhere.
Can I federate REST services?
Not directly. Each subgraph must expose a GraphQL schema that follows the Apollo Federation spec. You can wrap a REST API in a thin GraphQL subgraph if needed.
Should every field be @shareable?
No. Only use @shareable when the same field can be resolved by more than one subgraph. For most
extensions, @key and @external are enough.
Related Resources
GraphQL Schema Stitching Pattern
Merge multiple independent GraphQL schemas into a single unified schema that clients can query as one graph.
PatternGraphQL Connection Pagination Pattern
Implement Relay-style cursor-based pagination with edges, nodes, and pageInfo for stable GraphQL list queries.
RecipeSet Up a GraphQL Federation Gateway with Apollo
Compose multiple GraphQL services into a single federated supergraph using Apollo Federation and a gateway that routes queries across subgraphs
PatternGraphQL Mutation Validation: Centralized Input Checks
Centralize GraphQL mutation validation with reusable rules, custom scalars, and structured errors. Includes TypeScript examples and best practices.
DocGraphQL Federation Onboarding Template
Template for onboarding a service to a federated GraphQL graph: subgraph setup, entity definitions, resolver configuration, gateway integration, testing, deployment, and monitoring with code examples.
GuideComplete Guide to GraphQL Federation
Build unified GraphQL APIs across multiple services with Apollo Federation. Covers subgraphs, supergraph composition, entity resolution, and gateway deployment.