Combining @provides and @requires for Cross-Subgraph Fields

When a single resolver needs to both read fields owned by another service and avoid an extra network hop to fetch a field it could supply directly, @provides and @requires work together to encode that intent in the schema. This page shows how to combine them correctly, what @external is doing underneath, and how each directive changes the query plan.

These two directives are frequently confused because both reference fields the current subgraph does not own. They solve opposite problems. @requires declares an input dependency — the gateway must fetch external fields before your resolver runs. @provides declares an output optimisation — your resolver can return a field belonging to a referenced entity, so the gateway skips a round trip to that entity’s owner. Getting both into one realistic schema is the clearest way to see the distinction. Start from the parent guide, Using @external and @requires for Field Resolution, if you have not yet wired a single @requires field.

When to use this pattern

  • Use @provides when a resolver already loads a related entity’s data as a side effect of its own query, and you want to hand that field to the gateway directly instead of letting it dispatch a second _entities fetch to the owning subgraph.
  • Use @requires when a local computed field cannot be produced without one or more fields that live in another subgraph, and you want the gateway to deliver those fields into your resolver’s parent argument.
  • Combine both when one subgraph both consumes external fields (to compute something) and can opportunistically supply an external field it happens to have on hand — a common shape for a service that joins across two upstreams in a single query.

Prerequisites

The three directives, side by side

@external is the prerequisite for both of the others. It marks a field definition as a stand-in for a field the current subgraph does not own — a declaration that “this field exists, but its source of truth is another service.” On its own, @external does nothing useful; it only becomes meaningful when a @requires or @provides (or a @key on an extended type) references it.

@requires(fields: "...") attaches to a field your subgraph does own and lists external fields the gateway must resolve first. The planner inserts a fetch to the owning subgraph, then passes the results into your resolver’s parent.

@provides(fields: "...") attaches to a field that returns an entity owned by another subgraph, and promises that, along the path through this field, your subgraph can supply the named external fields itself. The planner trusts that promise and prunes the follow-up fetch — but only for that path.

Directive Attaches to References Effect on query plan Resolver responsibility
@external a field definition n/a (it is the target) none alone none; it is a marker
@requires a field you own external fields on the same entity adds a pre-fetch to the owner before your resolver read injected fields from parent
@provides a field returning another entity external fields on the returned entity prunes a fetch to that entity’s owner on this path actually populate the promised fields

The most important and most violated rule sits in the last column: @provides is a promise the planner believes. If your resolver returns the entity reference without the provided field actually populated, the gateway will not re-fetch it, and clients will silently receive null.

Effect of @requires and @provides on the federated query plan Two execution paths. With @requires the planner fetches external fields from the owner before the local resolver runs. With @provides the planner skips the follow-up fetch because the local subgraph supplies the field. @requires — adds a pre-fetch Gateway query planner Owner subgraph fetch external fields Local subgraph computes field 1. pre-fetch 2. inject parent @provides — prunes a follow-up fetch Gateway query planner Local subgraph supplies field too Owner subgraph fetch skipped 1. fetch 2. pruned @requires pulls inputs in before your resolver; @provides lets your resolver hand outputs back, saving a hop.

Implementation walkthrough

Consider three subgraphs. Users owns User. Products owns Product and holds each product’s priceCents and supplierId. Orders owns Order, references both User and Product, and — critically — loads the related product row in the same SQL join it uses to fetch the order line. Orders therefore has the product’s priceCents on hand without an extra call.

Orders wants two things. First, a computed lineTotalCents field on OrderLine that needs the product’s priceCents (an input dependency → @requires). Second, when a query traverses OrderLine.product, Orders can already return priceCents from its join, so it should provide that field and spare the gateway a fetch back to Products (an output optimisation → @provides).

# === orders subgraph ===
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.9",
        import: ["@key", "@external", "@requires", "@provides"])

# Product is OWNED by the products subgraph. Orders only extends it to
# reference it and to borrow priceCents. Every borrowed field is @external.
type Product @key(fields: "id") {
  id: ID!
  priceCents: Int! @external   # source of truth lives in products subgraph
}

type OrderLine @key(fields: "id") {
  id: ID!
  quantity: Int!

  # @requires: lineTotalCents is computed locally but needs the product's
  # external priceCents delivered into the resolver first.
  lineTotalCents: Int! @requires(fields: "product { priceCents }")

  # @provides: when this path is taken, Orders supplies priceCents itself
  # (it already loaded it via the order-line join), so the gateway will NOT
  # dispatch a second _entities fetch to products for priceCents here.
  product: Product! @provides(fields: "priceCents")
}

The matching resolvers. Note how lineTotalCents reads priceCents from parent.product (the gateway injected it because of @requires), and how the product resolver must actually populate priceCents to honour the @provides promise.

import { GraphQLResolverContext } from "./context";

interface OrderLineRow {
  id: string;
  quantity: number;
  productId: string;
  productPriceCents: number; // loaded in the same join as the order line
}

export const resolvers = {
  OrderLine: {
    // The gateway guarantees parent.product.priceCents is present because
    // lineTotalCents declares @requires(fields: "product { priceCents }").
    lineTotalCents: (parent: { quantity: number; product: { priceCents: number } }) => {
      return parent.quantity * parent.product.priceCents;
    },

    // Because OrderLine.product is annotated @provides(fields: "priceCents"),
    // we MUST return priceCents here. If we returned only { id }, the gateway
    // would trust the @provides promise, skip the products fetch, and clients
    // would see priceCents: null. The whole point is to fill it in.
    product: (parent: OrderLineRow) => ({
      __typename: "Product",
      id: parent.productId,
      priceCents: parent.productPriceCents, // satisfies the @provides contract
    }),
  },

  Product: {
    // Standard entity hydration for paths where Orders did NOT provide the
    // field — e.g. a query that asks for priceCents through some other route.
    __resolveReference: async (
      ref: { id: string; priceCents?: number },
      ctx: GraphQLResolverContext,
    ) => {
      if (typeof ref.priceCents === "number") return ref; // already provided
      return ctx.dataSources.products.byId(ref.id);
    },
  },
};

The @provides field is opportunistic and path-specific. If a different query reaches Product.priceCents without going through OrderLine.product — say directly from the Products subgraph — the planner uses the normal owner fetch. @provides only prunes the fetch on the exact path where the promise is declared. This is why the Product.__resolveReference above still guards against a missing priceCents.

Verification steps

Compose locally and confirm the supergraph builds with both directives present:

rover supergraph compose --config ./supergraph.yaml > supergraph.graphql
rover subgraph check my-graph@current \
  --schema ./orders/schema.graphql --name orders

Then inspect the query plan to confirm the @provides path actually prunes the Products fetch. Run a representative operation through the router with plan logging enabled:

APOLLO_ROUTER_LOG=apollo_router::query_planner=trace ./router --supergraph supergraph.graphql

Issue this operation and read the plan:

query {
  order(id: "o_1") {
    lines {
      quantity
      lineTotalCents      # exercises @requires
      product { priceCents } # exercises @provides
    }
  }
}

In the logged plan you should see a Fetch(service: "orders") that already selects product { priceCents }, and no follow-up Fetch(service: "products") for priceCents on that path. The expected response carries a non-null priceCents and a lineTotalCents equal to quantity * priceCents. If priceCents comes back null, the resolver is not honouring the @provides promise.

Common mistakes & gotchas

Declaring @provides but not populating the field. This is the silent killer. The planner prunes the owner fetch, the resolver returns only { id }, and the field resolves to null with no error. Always populate every field named in a @provides selection.

Forgetting @external on the borrowed field. Both @requires(fields: "product { priceCents }") and @provides(fields: "priceCents") reference Product.priceCents, which Orders does not own. It must be marked @external in the Orders SDL or composition fails with a field-ownership error.

Using @requires where @provides was meant (or the reverse). If you want to save a hop on output, that is @provides. If you need external data to compute, that is @requires. Mixing them up produces schemas that compose but plan inefficiently — extra fetches, or computed fields that never receive their inputs.

Frequently Asked Questions

Do @provides and @requires ever reference the same field?

They can, as in the example above where both touch Product.priceCents. That is the canonical “one subgraph joins across upstreams” shape: Orders needs priceCents to compute lineTotalCents (@requires) and can also hand priceCents back to the gateway on the product path (@provides). The field is marked @external once and reused by both directives.

Will @provides reduce latency for every query that touches the field?

No — only for paths that go through the field carrying the @provides annotation. A query reaching priceCents by another route still triggers the owner fetch, which is why your __resolveReference must remain correct. Treat @provides as a targeted optimisation, not a global one. For broader latency work, see Optimizing Reference Resolvers for Performance.

Can I @provides a field that itself requires a sub-selection?

Yes; @provides(fields: "...") accepts a selection set, including nested fields and multiple fields separated by spaces. Every field you name must be populated by your resolver and marked @external on the referenced type, or composition will reject the schema.