In a job that keeps a running total per customer, why can a stateful step read only the current record's customer total?
answer
- addressed, not searched
- the record picks the key
- one entry per key, per step
- no handle to another key's entry
basics
~20 sKey-bound state is addressable only under the grouping key of the record in hand. Each key's entry lives with the worker that owns that key, and the step is handed no way to reach another key's entry.
solid answer
~50 sThe step is using **key-bound state**: state a step may read or write only under the grouping key of the record it is currently handling, held by the worker that owns that key, so the read needs no coordination with any other worker. The step does not choose the key — the record does. When a record for customer `4471` arrives, the handle to the retained set resolves to that customer's entry and to nothing else; on the next record it resolves to a different one. If the program genuinely needs another customer's value, that is not a state read at all: you re-key and emit a record under the key you want to affect, aggregate at a coarser grouping key downstream, or join against the other dataset so the runtime co-locates the two sides.
go deeper
Recall the rule itself: a stateful step reaches only the entry for the key of the record in front of it, and the runtime routes every record for a key to one worker. Saying that clearly is the whole expectation here.
Explain why the scoping exists rather than restating it: single ownership per key gives the entry one writer, which turns the read into a local lookup. Name the upstream redistribution by key as the thing that makes it hold.
Show the restructuring you reach for when a step needs a value it cannot address — re-key and emit, aggregate at a coarser key, or join so the runtime co-locates the sides — and say which one you would defend in a design review and why.
The call you own is how many distinct groupings a pipeline is allowed to carry. Each grouping buys local reads and costs a redistribution, so a written standard for that tradeoff is cheaper than relitigating it in every job.
## What the job is holding, and how it is addressed A long-running job keeps a **retained set**: everything it is still holding between records — running accumulators, records buffered awaiting a group, deduplication entries, both sides of a join, and pending per-key callbacks. A running total per customer is one of those accumulators. It is not an ordinary variable in your code; it is an entry the runtime stores on the job's behalf, and the rule for reaching it is what this question is about. That rule is **key-bound state**: state a step may read or write only under the grouping key of the record it is currently handling, held by the worker that owns that key, so the read needs no coordination with any other worker. When a record for customer `4471` enters the step, the step's handle to the retained set resolves to customer `4471`'s entry. On the next record, for customer `9`, the same handle resolves to a different entry. **The step never selects the key; the record in hand selects it.** ## What the step may and may not touch | What the program wants | A key-bound state read? | What it actually is | |---|---|---| | The total for the customer in hand | Yes | The one entry the handle resolves to | | The total for some other customer | No | Another key's entry, not addressable from here | | A total across all customers | No | A second grouping at a coarser key, downstream | | A value looked up by a different identifier | No | A join, which the runtime co-locates on that identifier | The important line in that table is the second row. It is not that reaching another customer's entry is slow — it is that the step is not given a handle capable of naming it. ## Why the scoping exists - **One owner.** Upstream of the step the runtime performs a **redistribution by key** — the step that sends every record to the worker that owns its key — so one worker sees all of a customer's records. - **One writer.** With a single owner, the entry has a single writer, and a read-then-update needs no lock and no agreement with anybody. - **No directory.** No worker has to discover where other keys live, so there is no lookup service sitting in the per-record path. - **A bounded blast radius.** Losing a worker loses the entries it owned, not the whole retained set. The scoping is therefore not a restriction the runtime imposes for tidiness. It is the price of a purely local read, and it is paid by the redistribution upstream. ## When you genuinely need another key's value 1. **Re-key and emit.** Produce a record carrying the value, keyed by the customer you want to affect, and let the redistribution deliver it to that customer's owner. The other key's entry is then updated by its own owner, under its own key. 2. **Aggregate at a coarser key.** A total across customers is a second grouping — by region, by day, or by a single constant key when the cardinality is genuinely one — and it is a separate stateful step with its own entries. 3. **Join.** If the value you want belongs to another dataset, express it as a join on that identifier; the runtime arranges both sides so the match is local, rather than you reaching across from inside a state read. What all three have in common is that the cross-key access becomes a visible step in the job's structure instead of a hidden remote read inside a function. ## Where engines differ - In a **record-at-a-time** runtime — the model in which each record is handled as it arrives and touches the retained set individually — the handle re-resolves on every record, so per-access cost is paid once per record. - In the **repeated small finite jobs** model — the runtime cuts an endless input into short finite chunks and runs a complete job over each one — the step is handed a key and its stored value once per chunk per key, so the same addressing rule holds but the access happens far less often. - In the **two-phase disk-to-disk batch model**, which writes every intermediate result to disk between two fixed phases and keeps nothing between runs, there is no long-lived retained set at all, so the question does not arise: a grouping phase sees all the values for one key together and then the job ends. - Some engines additionally let a step register a **per-key scheduled callback** — a wake-up registered against one key and one moment. When it fires, the same scoping applies: the callback is bound to the key it was registered under. ## What an interviewer is listening for The candidate who has only used the feature says "you can store things per key." The candidate who understands it says why: the addressing rule and the routing are the same design decision seen from two ends, and it is what makes the read cost nothing. The follow-up is almost always the one about wanting some other key's value, and the good answer restructures the job rather than reaching for a lookup in the middle of a function.
- One step groups by customer and a later step groups by region. Does the region step see the customer entries?No. Each stateful step addresses its own entries under its own grouping key, and the two keyspaces are unrelated. Moving from one grouping to the other means another redistribution by key, after which the region step builds its own entries from the records it receives. Nothing is carried across automatically.
- Can the step write an entry for a key other than the one it is handling?No — the write is scoped exactly as the read is. The way to affect another key is to emit a record keyed for it, so the redistribution delivers it to that key's owner, which then performs the write itself under its own key. That makes the cross-key effect an explicit edge in the job.
saying these in an interview costs you the question
- Thinks a stateful step can iterate over every key's stored entry
- Says the step fetches another key's entry over the network
- Assumes two steps grouped differently share one stored entry
- Treats the retained set as a shared global map of all keys
- Believes the author chooses which key the handle resolves to