skip to content

How does a client reassemble a deferred GraphQL response from its incremental payloads?

level: middleimportance: should knowfreq 26%

answer

  1. hold a tree, not a parsed document
  2. each payload says where it goes
  3. field names and list indices from the root
  4. label separates two payloads at one path
  5. only hasNext false means complete

basics

~20 s

It keeps the initial payload's tree and merges each later payload into it at the response path that payload carries — a list of field names and list indices counted from the root. A payload whose hasNext is false ends the response.

solid answer

~50 s

The client parses payloads out of one streaming response body as they arrive and holds a mutable tree. The initial payload becomes that tree, with deferred fragments simply absent from it. Each later payload carries a `path` — field names and integer list indices from the root, the same shape used for error paths — and the client walks that path and merges the payload's fields into the object it finds. If the document supplied a `label`, it comes back too, and it is what distinguishes two deferred fragments hanging off the same path. `hasNext` is `true` on every payload except the last, where `false` is the only signal that the response is complete. Merging is purely additive: a deferred field was never delivered, so nothing is overwritten. Field errors in a later payload arrive after the HTTP status was already committed.

code

json · 9 lines
json
{
  "label": "enrichment",
  "path": ["account", "statements", 4],
  "data": {
    "disputedCount": 2,
    "merchantsResolved": 137
  },
  "hasNext": true
}

go deeper

for a junior

Know that the client keeps one growing tree and that each later payload says where its data belongs, and that the response is not finished until a payload says so explicitly.

for a middle

Explain the mechanics precisely: path as field names plus list indices from the root, label as the identity of the deferred unit, hasNext as the single termination signal, and merging as additive rather than overwriting.

for a senior

Demonstrate the failure handling — an ended body with hasNext still true is truncation, not completion, and field errors that arrive after a committed 200 will fool anything that judges success by status code.

for a principal

Own the portability risk: the envelope's exact keys have moved across drafts. Insist that client code depends on path, label and hasNext, and that any hand-rolled parser is justified against using a client that already speaks the framing.

## The client's job A client that supports incremental delivery is running a small merge loop, not a JSON parse. It reads payloads out of one streaming response body as they arrive, keeps a mutable response tree, and grafts each payload into that tree at the location the payload names. Three fields do all the work. **`path`** — an array of field names and integer list indices, counted from the root of `data`, exactly the same shape the specification already uses for error paths. `["account", "statements", 4]` means *the fifth element of the `statements` list on `account`*. The client walks that path in the tree it already holds and merges the payload's fields into the object it finds. If the payload arrives for a path whose parent is missing, the client has a bug or a malformed response — there is no ordering rule that lets a child arrive before its parent's container. **`label`** — the static string the client wrote in `@defer(label: "...")` or `@stream(label: "...")`, echoed back. `path` says *where*; `label` says *which deferred unit produced this*. That distinction matters when two deferred fragments hang off the same object: both payloads carry `path: ["account"]`, and only the label tells them apart. Drafts require the label to be a literal string, unique within the document, so a client can match it without evaluating variables. **`hasNext`** — a Boolean on every payload. `true` means more payloads are coming. `false` appears exactly once, on the last payload, and is the **only** completion signal. There is no trailing sentinel object, and the HTTP status is useless for this purpose because it was committed with the first bytes. ## Merging is additive A deferred fragment's fields are absent from the initial payload — not present-and-null. So a later payload never contradicts data the client already rendered; it fills a hole. This is why a naive merge is safe and why a UI can render the initial tree without defensive checks beyond "is this section here yet". Streamed list items behave the same way, with the index in the path doing the placing. Drafts differ on whether a streamed payload delivers one item or a batch of items appended at the tail of the list, which is another reason to hold the shape loosely and the invariants tightly. ## Framing The payloads travel as parts of one response body, conventionally `multipart/mixed` with a boundary, each part its own JSON document. The client must parse *incrementally* — a client that waits for the body to end before parsing has implemented incremental delivery and thrown away its only benefit. This is the single most common implementation mistake when a team hand-rolls the transport instead of using a client that already speaks it. ## Errors that arrive after the status A field error inside a deferred fragment produces an `errors` entry in the payload that carries it, with its own `path`. The response is already a 200, and it will stay a 200. Anything that decides success by status code — a retry wrapper, a dashboard, a synthetic check — will call a partially failed response healthy. That is a transport property of incremental delivery, not an error-handling nicety. ## Completion versus truncation Consider a statements screen with a deferred merchant-enrichment fragment inside each of twelve statement elements. Eleven payloads land. The twelfth never does, because the connection died mid-body — a laptop slept, a mobile network changed hands. What does the client have? A perfectly well-formed tree, eleven-twelfths filled, no error anywhere in it, a 200 status, and the last payload it read saying `hasNext: true`. Nothing distinguishes this from *still loading*, which is exactly why this failure presents as a screen that spins forever rather than as an error. The rule follows mechanically: **if the body ends and the last payload's `hasNext` was `true`, the response was truncated and must be treated as a failure.** A robust client also runs its own deadline for the remaining payloads and converts the timeout into a visible error state, or into a plain second operation that fetches the deferred fragment's fields on their own. ## The merge loop, stated plainly Start with `null`. First payload becomes the tree. Every later payload: walk its `path`, merge its `data`, collect its `errors`, and stop when `hasNext` is `false`. If the stream ends first, mark the tree truncated. That is the entire client contract, and being able to state it in five sentences is what the question is testing.

  • How does a client tell a finished incremental response from one that was cut off mid-stream?
    Only by `hasNext`. If the body ends while the last payload read said `true`, the response was truncated and must be treated as a failure. The status code cannot help — it was committed with the first bytes and stays 200 — and the tree itself looks perfectly well formed, just with holes. Without that check the symptom is a section that loads forever.
  • What does the label argument give a client that the path does not?
    Identity rather than location. Two deferred fragments on the same object produce payloads with identical paths, and only the label says which fragment each one answers. Drafts require it to be a literal string that is unique within the document, so a client can match it without evaluating variables at delivery time.
  • Can a later payload change a value that the initial payload already delivered?
    No. Deferred fields are absent from the initial payload rather than present and null, so later payloads only fill holes. That is what makes a naive additive merge safe, and it means a client can render the initial tree immediately without worrying that the values it painted will be revised.

saying these in an interview costs you the question

  • Buffers the whole body before parsing any payload
  • Treats the initial payload as the complete response
  • Reads a 200 status as proof every payload arrived
  • Ignores path and merges payloads in arrival order
  • Expects later payloads to overwrite delivered fields
  • Thinks hasNext appears only on the final payload

context