When does @provides in a federated subgraph actually save the router a fetch?
answer
- It removes a hop, on one route only
- Written on the field that returns the entity
- The promised fields are borrowed, not owned
- Same entity, different path, no saving
- Nobody checks what you handed back
basics
~20 sOnly when the client reaches the entity through the exact field carrying the directive, and only if the resolver really returns the promised values. Reached by any other path, the router still fetches the field from the subgraph that owns it.
solid answer
~50 s`@provides` is written on a field that returns an entity, and it names fields of that entity which this subgraph can already supply inline — typically because it holds a denormalised copy. The router then reads those fields from this subgraph's response instead of planning a follow-up call to the owner. Three conditions bound it. The provided fields must be declared `@external` in this subgraph, so it is clear they are borrowed rather than owned, and they must be resolvable in more than one place at the owner, which in Federation 2 means marked shareable. The promise is scoped to that one field path, so the same entity reached from a different field still costs the owner fetch. And nothing verifies it at runtime: if the resolver returns null or a stale value for a provided field, the router uses it, because it deliberately did not ask anyone else.
code
graphql · 16 lines# Reservations subgraph
type Reservation @key(fields: "id") {
id: ID!
locker: Locker! @provides(fields: "address")
}
type Locker @key(fields: "id") {
id: ID!
address: String! @external
}
# Lockers subgraph — the owner must allow a second resolver
type Locker @key(fields: "id") {
id: ID!
address: String! @shareable
}go deeper
Recall the shape: it sits on a field that returns another subgraph's entity, and it names fields of that entity this service can already return.
Explain the mechanics — why the named fields are external here, why the owner has to allow a second resolver, and how the router's plan differs with and without it.
Show the judgement: the saving exists only on the declared path, the promise is unverified, and a denormalised copy needs an owner and a reconciliation story before you rely on it.
Decide the policy. Weigh whether the organisation accepts duplicated truth for latency at all, which paths justify it against real operation traffic, and what evidence has to exist before a team ships one.
## The optimisation it expresses In a federated graph, a subgraph that returns another subgraph's entity normally returns only the key. The router then issues a second fetch to the owning subgraph, passing representations built from those keys, to fill in the fields the client actually asked for. That second fetch is the standard cost of splitting a type across services. `@provides` is how a subgraph says the second fetch is unnecessary *here*. It goes on the field that returns the entity, and it names the entity's fields this subgraph can already hand over: ```graphql # Reservations subgraph type Reservation @key(fields: "id") { id: ID! locker: Locker! @provides(fields: "address") } type Locker @key(fields: "id") { id: ID! address: String! @external } ``` Read literally: when a client walks from `Reservation` to `locker`, this subgraph will return `address` alongside the key, so plan for it here rather than calling the Lockers subgraph. That is usually possible because the reservation row already denormalises the locker address at booking time. ## The three conditions **The fields must be `@external` here.** `address` is not this subgraph's field; declaring it external says "I am naming someone else's field so I can talk about it". Without that, the declaration reads as a second implementation and runs into the ownership rules instead. **The owner must permit resolution in two places.** Once this subgraph returns `address`, that field is genuinely resolvable in two subgraphs, so in Federation 2 the owner must mark it shareable. This is the step people forget, and it fails at composition rather than at request time — which is the good outcome. **It is scoped to the one field path where it is written.** This is the condition that decides whether the optimisation shows up in production at all. The directive above says nothing about any other route to a `Locker`. A document that selects `locker(id: ...) { address }` at the root, or reaches a locker through `parcel { destinationLocker { address } }`, gets the ordinary owner fetch. So `@provides` pays off when one traversal dominates your traffic, and is close to dead weight when clients reach the entity a dozen ways. Before adding it, look at real operation traffic and ask which path carries the volume; adding it on a rarely-walked edge is complexity for nothing. ## Nothing checks the promise Composition validates the shape of the declaration — that the fields exist, are external here, and are shareable at the owner. It cannot validate the behaviour. At request time the router *skips asking the owner*, so whatever this subgraph puts in those keys is what the client gets. A concrete failure from a parcel-locker graph. The Reservations subgraph provided `Locker.address` from a denormalised column written at booking time. A locker was physically relocated; the Lockers subgraph was updated, the denormalised copies were not. Clients reaching the locker through a reservation received the old address for weeks, while the same field fetched from the root returned the correct one. No error, no log line, two truths in one graph — and the only signal was a support ticket. The list case is nastier. Put `@provides` on a field returning a list of entities and the promise applies to every element. In the same graph a `Locker.recentHolds` list grew without a bound as a locker aged; the resolver populated the provided field only for the elements it could serve from a warm cache and left the rest null. The router did not re-fetch — it had been told not to — so nulls flowed straight to clients, and because the provided field was non-null the nulls bubbled and erased whole branches of the response. The size of the blast radius was determined by how long the list had been growing, so it looked like a random, worsening intermittent bug. ## How to reason about it in an interview Say what it optimises (a fetch, on one path), what it costs (a second copy of the truth, plus a shareable field at the owner), and how it fails (silently, with stale or missing values, because skipping the owner fetch is the whole point). Contrast it with `@requires`, which points the other way: `@requires` lets a field in *this* subgraph demand sibling fields owned elsewhere, which forces the router to fetch from the owner *before* it can call here — adding a sequential step rather than removing one. `@provides` trades correctness risk for latency; `@requires` trades latency for expressiveness. A field that is expensive to keep in sync is a bad candidate for the first, and a field on a hot path is an expensive candidate for the second. When you do adopt it, treat the denormalised copy as a first-class integration: give it an owner, a refresh path and a reconciliation job that compares it against the owning subgraph, because the router will never do that comparison for you.
- How is @provides different in direction from @requires?`@provides` is an offer: this subgraph hands over fields it does not own, so the router can skip a call. `@requires` is a demand: a field here cannot be computed without sibling fields owned elsewhere, so the router must fetch those from the owner first and pass them in. One removes a step from the plan, the other inserts a sequential one.
- The provided field started returning stale values. What would you change?First establish whether it is stale everywhere or only on the provided path — comparing the same field fetched through the entity's own root field usually settles it in a minute. Then either fix the denormalisation properly, with an ownership story and a reconciliation job, or drop the directive and pay for the fetch. Keeping an unowned copy that only some queries can see is the worst of the three.
- Would you add @provides to a field returning a list of entities?Only with care, because the promise applies to every element, and a resolver that can populate the provided fields for some elements but not all will emit nulls the router accepts without question. If the list is unbounded or its backing data is only partly warm, the safer choice is the ordinary owner fetch, which at least handles every element the same way.
saying these in an interview costs you the question
- Thinks it applies to the type everywhere, not one path
- Says the router verifies the provided values
- Forgets the fields must be external in this subgraph
- Forgets the owner must allow a second resolver
- Treats it as free performance with no correctness cost
- Confuses it with demanding sibling fields from the owner