skip to content

In a federation query plan, what forces one subgraph fetch to wait for another?

level: middleimportance: must knowfreq 55%

answer

  1. Not the order fields were written
  2. One step consumes what another produces
  3. Key values cannot be invented early
  4. Cousins wait once, then go together
  5. Mutation roots are serial by specification

basics

~10 s

A data dependency. A fetch waits only when its input comes from an earlier fetch's result — usually the key values identifying its objects. Fetches whose inputs are already available run concurrently.

solid answer

~50 s

The planner orders steps by **dependency**, not by the order fields appear in the document. Two root fields owned by different subgraphs depend on nothing from each other, so their fetches are grouped and issued concurrently. An entity fetch is different: the router cannot build its representations until it holds the key values of the objects involved, so it must sit after the fetch that produced them. That is the sequential edge, and it is the only structural reason for one. Two sibling fields under the same parent that live in *two different* subgraphs both depend on that parent and on nothing else, so they run concurrently with each other after it. One rule comes from the GraphQL specification rather than federation: the top-level fields of a **mutation** execute in series, so a router must not parallelise those root steps even when they touch unrelated subgraphs.

code

pseudocode · 7 lines
pseudocode
Sequence
  Parallel
    Fetch -> Jobs         { jobFeed { id title employer { __typename id } } }
    Fetch -> Applications { myApplications { id status job { __typename id } } }
  Parallel
    At path [email protected]        Fetch -> Employers  (entity request: name)
    At path [email protected]      Fetch -> Jobs       (entity request: title)

go deeper

for a junior

Recall the headline: independent parts of a plan go out together, and anything that needs the identity of objects another fetch returned has to wait for it. That is enough at this level.

for a middle

Explain the mechanism precisely — representations need key values, key values come from an earlier result, therefore an edge. Then show you know two entity fetches under one parent form a concurrent group, not a chain.

for a senior

Demonstrate that you read width and depth separately in a trace, and that you know a flat list of subgraph spans hides the shape entirely. Be ready to name what in the schema created a sequential edge.

for a principal

Own the point that some ordering is authored, not inferred: a field declared to need sibling data buys correctness with a round trip. Decide when that trade is acceptable and how it is caught in review.

## Dependency, not document order The first thing to get right is that a planner does not walk the document top to bottom issuing calls. It builds a dependency graph and then schedules it. Two steps are ordered relative to each other only when one *consumes something the other produces*. Everything else is free to go out at once, subject to the router's own concurrency limits. What exactly gets consumed? Almost always, **key values**. When the router needs fields that another subgraph owns, it must address them: it sends representations — stubs carrying `__typename` and the key fields identifying each object. It cannot fabricate those values. They exist only once the subgraph holding the parent objects has answered. That is what makes an entity fetch sequential with respect to its parent fetch, and it is the whole mechanism behind plan depth. ## The three common orderings **Independent roots run together.** A job-board screen that asks for `jobFeed` (owned by the Jobs subgraph) and `myApplications` (owned by the Applications subgraph) in one query has two root fields that need nothing from each other. Both fetches leave at the same moment, and the query costs roughly one fetch latency rather than two. **Entity fetches wait for their parent.** Adding `employer { name }` under `jobFeed` does not add a concurrent fetch — it adds a *round*. The router must first learn which employers the feed contains, then take those identities to the Employers subgraph. **Cousins wait, then run together.** If the same screen also wants `job { title }` under `myApplications`, the plan now has two entity fetches: employers for the feed, and jobs for the applications. Neither depends on the other; both depend on the first round. So they form a second concurrent group. Four fetches, two rounds. ## Dependencies you create yourself Some ordering is authored rather than inferred. A subgraph can declare that one of its fields cannot be computed without sibling data owned elsewhere — an application's ranking needing the posting's seniority band, say. The planner then has to fetch that sibling data first and feed it into the fetch that produces the field, which is an extra sequential edge the schema author introduced. It is worth being explicit about this in an interview: some of the plan's shape is the router's inference, and some of it is a consequence of how the schema was written. ## The one ordering rule that is specified Most of the above is planner behaviour. One constraint is not. The GraphQL specification requires that the top-level selections of a **mutation** operation execute serially, in the order written, so that the second mutation observes the effect of the first. A router honouring that cannot fold two mutation root fields into a concurrent group even when they land on entirely different subgraphs. Query root fields carry no such rule and may be executed in any order or concurrently. Candidates who know only the federation half of this usually miss it, and it is a satisfying detail to volunteer. ## A dependency you can create by accident One pattern catches teams out. A field that looks like it belongs to the object in front of you may in fact be completed elsewhere, and selecting it turns a one-round plan into two without anything in the document hinting at it. On the job board, adding an employer's verification badge to a list of postings does not add a field to an existing fetch — it adds a whole round, because that badge lives in a different service. The cost is invisible in the composed schema, which is exactly the trade a composed graph makes: callers get one coherent type system, and the price of a selection stops being legible from the selection. ## Reading it back When you look at a plan, look at its **width and depth separately**. Width — how many fetches sit in one concurrent group — costs the router connections and costs subgraphs load, and it is bounded by whatever concurrency the router allows. Depth — how many groups are chained — costs wall-clock time, because each group waits for the one before it. A plan that is wide and shallow is usually healthy. A plan that is narrow and deep is the one that shows up in a latency complaint, and the way to shorten it is nearly always to change where fields live rather than to tune the router. One caution about diagnosis: a trace that lists subgraph spans as a flat list tells you very little. You need to see which spans overlap in time and which start only after another ends. Two plans with identical span lists can have completely different shapes, and only the nesting distinguishes them.

  • Two root fields of a query land on the same subgraph. Does the router send two fetches?
    It should not. Selections owned by one subgraph and available at the same point in the plan are normally merged into a single operation, so one round trip carries both root fields. That merging is planner behaviour rather than a specified rule, but it is the standard optimisation and the reason fetch count usually tracks distinct subgraph-and-round pairs rather than selected fields.
  • Why can two entity fetches under the same parent run concurrently?
    Because they consume the same input and produce disjoint output. Once the parent fetch has returned the objects and their key values, each entity fetch has everything it needs; neither reads the other's result, and each writes into a different branch of the response. The dependency edges run parent-to-child only, so the children form one concurrent group.
  • How does the ordering rule differ between a query and a mutation in a federated graph?
    Query root fields have no required order, so a router may group independent ones concurrently. Mutation root fields must execute in series in document order, per the GraphQL specification, so each becomes its own step and the subsequent one starts only after the previous completes. That makes a multi-root mutation across subgraphs inherently deep, whatever the topology.

saying these in an interview costs you the question

  • Says plan steps always run in the order fields appear
  • Thinks every subgraph fetch is inherently sequential
  • Believes concurrency depends on how fast subgraphs are
  • Forgets that mutation root fields must execute in series
  • Cannot say what an entity fetch is actually waiting for

context