Apollo Federation: Cross-Subgraph Field Resolution Pattern
December 10, 2025
Overview
This post is a repackaged revision lesson for myself, explaining a common Apollo Federation pattern where one subgraph defines a query that returns entities, but the full entity details are resolved by a different subgraph.
The Pattern
Subgraph A (User Context): Knows which entities belong to a user Subgraph B (Entity Details): Knows all the details about those entities
Apollo Federation automatically connects them together.
Step-by-Step Example
Let’s walk through how this works with a concrete example.
Step 1: Define the Query
Subgraph A defines a query that returns user-specific data:
# schema/query.ts
type Query {
"""
Gets the user's item list
"""
userItems(pagination: PaginationInput): UserItemsResponse!
} Step 2: Define the Response Type
The response type contains a reference to entities from another subgraph:
# schema/responses.ts
type UserItemsResponse {
items: ItemList!
}
type ItemList {
pageInfo: PageInfo!
records: [Item!]!
} Step 3: Create the Entity Stub
Subgraph A defines a “stub” type with only the key field and the @key directive:
# schema/type.ts
type Item @key(fields: "id") {
id: ID!
} Key points:
- The
@key(fields: "id")directive tells Apollo Federation: “This entity can be identified by itsid” - Only the
idfield is defined - this is a stub type - This signals to the gateway: “I don’t have all the fields, look elsewhere”
Step 4: Implement the Resolver
The resolver in Subgraph A only needs to return the IDs:
// resolvers/queries/index.ts
const resolvers = {
Query: {
async userItems(
_: unknown,
args: { pagination?: PaginationInput },
ctx: AuthenticatedContext
): Promise<UserItemsResponse> {
// Fetch which items belong to this user
const response = await ctx.dataSources.userApi.getUserItems(ctx.user.userId, args.pagination);
const items = response.content.items ?? [];
return {
items: {
pageInfo: getPageInfo(response.result.count, items.length, args.pagination),
records: items.map((item) => {
return {
id: item.itemCode // Only return the ID!
};
})
}
};
}
}
}; Notice: The resolver only returns { id: item.itemCode } - nothing else!
Step 5: Full Entity Definition in Another Subgraph
Subgraph B has the complete entity definition with all fields:
# Subgraph B: schema/type.ts
type Item @key(fields: "id") {
id: ID!
name: String!
description: String!
imageUrl: String!
price: Float!
category: Category!
inStock: Boolean!
metadata: ItemMetadata!
} Subgraph B also implements a reference resolver:
// Subgraph B: resolvers/types/item.ts
const Item = {
__resolveReference(reference: { id: string }, ctx: Context) {
// Fetch full item details using the ID
return ctx.dataSources.itemApi.getItemById(reference.id);
}
}; How Federation Magic Works
When a client makes the query:
query {
userItems(pagination: { limit: 10 }) {
items {
records {
id
name
description
price
}
}
}
} Here’s what happens behind the scenes:
- Apollo Gateway routes the
userItemsquery to Subgraph A - Subgraph A resolver executes and returns:
{ "items": { "records": [{ "id": "ITEM001" }, { "id": "ITEM002" }, { "id": "ITEM003" }] } } - Apollo Gateway sees that the client requested more fields (
name,description,price) that aren’t in Subgraph A - Apollo Gateway looks up which subgraph has the full
Itemdefinition (Subgraph B) - Apollo Gateway calls Subgraph B’s
__resolveReferencewith the IDs - Subgraph B fetches and returns full item details
- Apollo Gateway merges the results and returns to the client:
{ "items": { "records": [ { "id": "ITEM001", "name": "Product 1", "description": "...", "price": 29.99 }, { "id": "ITEM002", "name": "Product 2", "description": "...", "price": 49.99 }, { "id": "ITEM003", "name": "Product 3", "description": "...", "price": 19.99 } ] } }
Type Extension Pattern
You can also extend types from another subgraph to add user-specific fields:
# Subgraph A: schema/type.ts
extend type Item @key(fields: "id") {
id: ID! @external
isInUserWishlist: Boolean
userRating: Int
} Directives used:
extend type- Adds fields to an existing type from another subgraph@external- Marks fields that are defined in the other subgraph@key(fields: "id")- Identifies how to reference this entity
Resolver in Subgraph A:
// Subgraph A: resolvers/types/item.ts
const Item = {
async isInUserWishlist(
parent: { id: string },
_args: unknown,
ctx: AuthenticatedContext
): Promise<boolean> {
return await ctx.dataSources.wishlistApi.isInWishlist(ctx.user.userId, parent.id);
},
async userRating(
parent: { id: string },
_args: unknown,
ctx: AuthenticatedContext
): Promise<number | null> {
return await ctx.dataSources.ratingsApi.getUserRating(ctx.user.userId, parent.id);
}
}; Now when a client queries:
query {
userItems {
items {
records {
id
name # from Subgraph B
price # from Subgraph B
isInUserWishlist # from Subgraph A
userRating # from Subgraph A
}
}
}
} Apollo Federation automatically:
- Gets the item IDs from Subgraph A
- Fetches base item details from Subgraph B
- Fetches user-specific fields from Subgraph A
- Merges everything together
Key Takeaways
- Stub Types: Define minimal types with
@keydirective to reference entities from other subgraphs - Separation of Concerns:
- One subgraph knows which entities to return
- Another subgraph knows what data those entities contain
- Minimal Data Transfer: Subgraphs only return what they know (usually just IDs)
- Automatic Resolution: Apollo Gateway handles the rest
- Type Extension: Add subgraph-specific fields to entities owned by other subgraphs
Federation Directives Reference
| Directive | Purpose |
|---|---|
@key(fields: "id") | Marks the unique identifier field(s) for entity resolution |
@external | Indicates a field is defined in another subgraph |
extend type | Adds fields to a type from another subgraph |
Benefits of This Pattern
- Domain Separation: Each subgraph owns its domain data
- Independent Development: Teams can work on subgraphs independently
- Scalability: Subgraphs can be deployed and scaled separately
- Flexibility: Easy to add new fields without modifying other subgraphs
- Type Safety: Full GraphQL type safety across subgraphs