Using @external and @requires for Field Resolution
In a distributed GraphQL graph, some fields cannot be computed without data that another service owns, and resolving them correctly means declaring that dependency in the schema rather than reaching across a network boundary by hand. The @external and @requires directives form the contract that lets a subgraph borrow a sibling’s fields at planning time: the router fetches the owning subgraph’s data first, threads it into the entity reference, and only then invokes your local resolver. This is one of the core skills covered under Subgraph Implementation & Entity Resolution, and it is where most teams first hit composition errors and surprise N+1 fetches in production.
This guide explains the directive semantics, the exact SDL and resolver wiring you need, how the query planner turns a @requires declaration into fetch nodes, and how to keep the resulting execution plans fast. Two focused walkthroughs go deeper on specific patterns: implementing @requires for computed fields and combining @provides and @requires for cross-subgraph fields.
Problem Statement
A subgraph rarely owns every field it needs to do its job. A pricing service may compute discountedTotal, but the base price and currency live in a catalog service. A reviews service may want to flag isVerifiedPurchase, but the order history that proves the purchase belongs elsewhere. Without federation directives, the only options are duplicating that upstream data (which drifts) or making a synchronous call from inside a resolver (which the router cannot see, optimize, or trace). Both undermine the boundaries that made you split the graph in the first place. @external and @requires solve this by making the dependency declarative: the schema states exactly which foreign fields a computation needs, the router satisfies them as part of the query plan, and your resolver receives them already populated on the reference object.
Prerequisites
Concept Deep-Dive: Directive Semantics & Schema Contracts
The @external directive marks a field on an entity as belonging to another subgraph. It says, in effect, “I am not the authority for this field, but I need to reference it.” On its own @external does nothing but inform composition; it becomes meaningful when paired with @requires, @provides, or @key.
The @requires(fields: "...") directive attaches to a field your subgraph does own and lists the external fields the router must fetch before your resolver runs. The argument is a GraphQL selection set, so it can name scalar fields ("price currency") or descend into nested objects ("dimensions { width height }"). At composition time the router validates that every field named inside @requires is reachable on the entity and is marked @external in this subgraph; at runtime it builds a fetch that pulls those fields from the owning subgraph and injects them into the entity representation passed to your __resolveReference or field resolver.
The critical mental model is that @requires reshapes the query plan, not just the resolver. The router inserts an extra fetch against the owning subgraph, gated on the entity’s key, and feeds the result forward. That is powerful, but it means a careless @requires can add a serial network hop to every request that touches the field.
There is a second, subtler property worth internalizing: @requires does not give your subgraph ownership of the borrowed fields. You cannot expose price to clients from the pricing subgraph just because you required it — it remains owned by catalog, and a client selecting price will still be served by catalog’s resolver. The required values exist only transiently, inside the entity representation, for the duration of your computed field’s resolution. They are an input to your logic, never an output of your schema. Confusing the two is the most common conceptual mistake teams make when they first reach for the directive, and it leads to attempts to “re-export” required fields that composition will either reject or silently route elsewhere.
It is also worth being precise about when the required fetch happens. The router does not eagerly fetch every @requires dependency on every query — it only plans the extra fetch when the operation actually selects the computed field. A query that asks for Product { id name } never triggers the pricing fetch at all; only Product { id discountedTotal } does. This is why @requires is relatively safe to add to fields that are rarely selected, and relatively dangerous to add to fields that appear in hot, high-fan-out list queries.
# pricing subgraph
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.9",
import: ["@key", "@external", "@requires"])
type Product @key(fields: "id") {
id: ID!
# owned by the catalog subgraph; declared external so we can require it
price: Float! @external
currency: String! @external
# owned here; needs the two external fields above to be computed
discountedTotal(coupon: String): Float! @requires(fields: "price currency")
}
How @external differs from @shareable and @provides
It is easy to confuse @external with related directives. @shareable means two subgraphs can each resolve a field — they are co-owners. @external means this subgraph cannot resolve the field at all and is only referencing it. @provides is the inverse optimization of @requires: it lets a subgraph that happens to have a foreign field handy return it inline so the router can skip a hop. The interplay of @provides and @requires is subtle enough that the dedicated guide on combining @provides and @requires for cross-subgraph fields walks through it end to end.
Directive & Config Spec Table
| Directive | Where it sits | Argument | Composition-time effect | Runtime effect |
|---|---|---|---|---|
@external |
A field on an entity type in a non-owning subgraph | none | Marks the field as not resolvable here; required before @requires/@provides can name it |
None directly; the field is excluded from this subgraph’s resolution unless provided |
@requires |
A field this subgraph owns | fields: "<selection set>" |
Validates every named field is @external and reachable on the entity’s @key |
Router fetches the named fields first and injects them into the reference object |
@provides |
A field returning an entity, or on the entity field | fields: "<selection set>" |
Validates the named fields are @external here and owned elsewhere |
Lets the router skip a hop when this subgraph can return the foreign fields inline |
@key |
An entity type | fields: "<selection set>", resolvable |
Defines the identity used to fetch the entity across subgraphs | Drives _entities representations and __resolveReference |
Step-by-Step Implementation
1. Mark the foreign fields @external
In the subgraph that needs to borrow data, redeclare the foreign fields on the entity and annotate each with @external. The types must match the owning subgraph’s SDL exactly.
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.9",
import: ["@key", "@external", "@requires"])
type Product @key(fields: "id") {
id: ID!
price: Float! @external
currency: String! @external
}
2. Declare the computed field with @requires
Add the field your subgraph owns and attach @requires, naming only the external fields the computation truly needs.
type Product @key(fields: "id") {
id: ID!
price: Float! @external
currency: String! @external
discountedTotal(coupon: String): Float! @requires(fields: "price currency")
}
3. Wire the resolver to read the injected fields
Your resolver receives the required fields on the reference (parent) object. Do not re-fetch them — the router already paid for that fetch.
interface ProductRef {
__typename: 'Product';
id: string;
price?: number; // injected by the router because of @requires
currency?: string; // injected by the router because of @requires
}
interface Context {
rates: { convert(amount: number, from: string): Promise<number> };
coupons: { discountFor(code: string): Promise<number> };
}
export const resolvers = {
Product: {
discountedTotal: async (
ref: ProductRef,
args: { coupon?: string },
ctx: Context,
): Promise<number> => {
// The router guarantees these because of @requires(fields: "price currency").
if (ref.price == null || ref.currency == null) {
throw new Error('Missing required external fields: price/currency');
}
const usd = await ctx.rates.convert(ref.price, ref.currency);
const discount = args.coupon ? await ctx.coupons.discountFor(args.coupon) : 0;
return Number((usd * (1 - discount)).toFixed(2));
},
},
};
4. Compose and confirm the plan
Run composition locally and inspect the generated plan so you can see the extra fetch @requires introduces.
rover supergraph compose --config supergraph.yaml > supergraph.graphql
# Then run an operation through the router with tracing on:
APOLLO_ROUTER_LOG=trace ./router --supergraph supergraph.graphql
Composition Pipeline Integration
Treat @requires changes as schema contract changes, because they are. A reworded selection set or a renamed external field can break composition or silently change the fetch plan. Gate every subgraph publish behind rover subgraph check so the registry validates the directive against the live supergraph before it ships.
# .github/workflows/subgraph-check.yml
name: subgraph-check
on: [pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rover
run: curl -sSL https://rover.apollo.dev/nix/latest | sh
- name: Check pricing subgraph
env:
APOLLO_KEY: ${{ secrets.APOLLO_KEY }}
run: |
rover subgraph check "$APOLLO_GRAPH_REF" \
--name pricing \
--schema ./pricing/schema.graphql
This is the same discipline applied to every contract change across the graph; for the broader picture see schema validation in CI/CD pipelines.
Two failure categories deserve to be caught here rather than in production. The first is a composition break: you rename or retype an @external field and it no longer lines up with the owning subgraph, so rover subgraph check reports the field is not external or the type diverges. The second, more insidious, is an operation break: composition still succeeds, but the new @requires selection changes the query plan for an existing client operation in a way that adds latency or breaks a previously valid query. The managed-federation check compares your proposed schema against recent real operation traffic, so it can flag the second category that a purely structural check would miss. Wire both the structural check and the operation check into the same pull-request gate so a directive change cannot merge without proving it neither breaks composition nor degrades a live operation.
Performance & Scale Considerations
The defining cost of @requires is the extra, serially gated fetch. Because the router must collect the external fields before your resolver can run, that fetch sits on the critical path: under load it adds a full round trip to the owning subgraph for every entity that selects the computed field. A few rules keep this in check.
Scope the selection set ruthlessly. Every field you name in @requires is fetched and serialized for every entity in the batch, even if your computation only sometimes touches it. Naming "price currency" instead of a wide "price currency description weight dimensions" can shrink the _entities payload by an order of magnitude on wide types.
Watch for hidden N+1 fan-out inside the required fetch. If the owning subgraph resolves the external fields with an unbatched per-key database call, you reintroduce the N+1 the architecture was meant to remove. Wrap the owning resolver in a DataLoader — see optimizing reference resolvers for performance and the focused guide on batching entity resolution with DataLoader.
Cache the stable inputs. Computed fields that depend on slowly changing upstream data — tax tables, exchange rates, catalog prices — are strong candidates for an entity-level cache or router response cache so the required fetch can be skipped on repeat reads. The trade-offs are covered in caching strategies for federated GraphQL.
Prefer @provides where the data is already in hand. When a list resolver in another subgraph already loads the external fields as part of its own query, a @provides on that path lets the router skip the separate required fetch entirely, collapsing two serial hops into one.
Failure Modes & Debugging
Composition error — field not marked @external. Composition fails with a message like Field "Product.discountedTotal" requires "price" but it is not declared @external in subgraph "pricing". The fix is to redeclare price on the entity in the requiring subgraph and annotate it @external, with a type that matches the owning subgraph exactly.
Runtime — required field arrives null. Your resolver sees ref.price === undefined even though composition passed. This means the owning subgraph’s resolver dropped the field, or it returned null for that entity. Enable APOLLO_ROUTER_LOG=debug to inspect the _entities response, confirm the owning subgraph’s __resolveReference returns the field, and add a defensive guard so a partial upstream payload degrades gracefully rather than throwing. Partial-payload handling is covered in entity resolution fallback strategies for partial data.
Query planner — unresolvable dependency path. A nested selection like @requires(fields: "metadata { taxRate }") fails to plan when the path does not exist in the composed schema or uses the wrong syntax. Use space-separated names for top-level scalars and selection-set braces for nested objects, and validate the path against rover supergraph compose output.
Field type mismatch during composition. If the @external redeclaration uses Float where the owner declares Float!, composition rejects the type divergence. Mirror the owning SDL exactly, including nullability, and standardize shared scalars — see custom scalars in federated GraphQL schemas.
Frequently Asked Questions
Can I use @requires without @external?
No. Every field named in a @requires selection set must be declared @external in the same subgraph. The @external annotation is what tells composition the field is owned elsewhere; without it the router cannot tell the difference between a local field and a borrowed one, and composition fails.
Does @requires always add a network hop?
Effectively yes, unless the owning subgraph supplies the fields through @provides on the path that reaches the entity, or the fields are already resolved earlier in the plan. By default the router inserts a serial fetch against the owning subgraph to satisfy the requirement before your resolver runs.
How do I require a nested field?
Use a selection set in the fields argument: @requires(fields: "shipping { weight zone }"). The path must exist on the composed entity and each leaf must be @external in your subgraph. Top-level scalars are space-separated; nested objects use braces.
How does @requires interact with caching?
The computed field is only as fresh as its required inputs, so cache invalidation must account for upstream changes. Tag cached computed values with the owning entity’s key and invalidate them when the source fields mutate; see caching strategies for federated GraphQL.