skip to content

In a demand-driven stream, a consumer requests 10 values, then 5 more before any arrive — what may the producer send?

level: middleimportance: must knowfreq 68%

answer

  1. a count, not a rate
  2. the consumer names the number
  3. requests add up, never replace
  4. a ceiling, not an obligation
  5. one unit spent per value delivered

basics

~20 s

Up to 15 values, and not one more. Requests accumulate additively into a single outstanding count, each delivered value spends one unit, and the producer is barred from sending a sixteenth value until the consumer asks again.

solid answer

~40 s

Demand is an **outstanding count**, not a rate and not a deadline. Two requests add: 10 and then 5 leaves 15 permitted deliveries, not 5 and not the larger of the two. Every value handed downstream spends one unit; when the count reaches zero the producer must stop and wait, however much data it is sitting on. The count is a ceiling rather than an obligation — with 15 outstanding the producer may legitimately deliver two values, or none, and a quiet source is not a stall. Only values are charged against the counter: the signal that ends the sequence, normally or with a failure, needs no demand. A consumer may also request an effectively unbounded count, which is how it deliberately opts out of flow control altogether.

code

pseudocode · 15 lines
pseudocode
outstanding = 0

on requestSignal(n):                  // n arrives from the consumer
    if n <= 0:
        failSequence("non-positive request")
        return
    outstanding = outstanding + n     // additive: it never replaces
    drain()

function drain():
    while outstanding > 0 and hasNextRow():
        emit(nextRow())
        outstanding = outstanding - 1
    if noMoreRows():
        signalComplete()              // charged against nothing

go deeper

for a junior

Recall the shape: the consumer says how many values it can take, and the producer may not send more than that. The number is a count of values, not a speed.

for a middle

Explain the accounting out loud: requests accumulate into one outstanding count, each delivered value spends a unit, zero means stop. Be able to add two overlapping requests correctly and to say that the count is a ceiling rather than a promise.

for a senior

Show you have watched this counter in production: a stream that stalls because nobody replenished, a stage that over-delivered because the decrement raced the increment, and the difference between an idle source and a starved one.

for a principal

Frame the counter as the contract between two teams. Batch size trades signal chatter against how much data is in hand, and an unbounded request is an explicit waiver of flow control that should be visible in review, not buried.

## What a request actually says In a demand-driven stream the **consumer** is the side that names a number. It signals upstream *I can take N more values*, and from that moment the producer holds a budget of N deliveries that it may not exceed. Nothing about that number is a rate: it is not *N per second*, it does not expire, and it carries no schedule. It is an **outstanding count** that lives until it is spent or the subscription ends. A worked setting makes this concrete. A report writer is exporting a ledger. It can hold one page of rows in memory, validate them and commit them, and only then take more. So it asks for a page's worth. The producer — a query walking a very large result — may hand over at most that many rows, then must stop and wait, no matter how many rows it already has staged. ## Why requests add rather than replace The decisive detail is what happens when the consumer asks again before the first request has been filled. The counts **accumulate**. A request for 10 followed by a request for 5 leaves 15 permitted deliveries. This is what makes the protocol usable from the consumer's side. A consumer that has just committed a batch does not have to know how much of its earlier request is still outstanding, nor coordinate with the producer about it; it simply adds the amount of capacity it has newly freed. Two replenishment shapes fall out: - **One-for-one** — ask for one more value for each value processed. The outstanding count hovers near a constant, and you pay one request signal per value. - **Batched** — ask for a page, let it drain to a low-water mark, then ask for another page. Far fewer signals, at the cost of holding more in hand. If requests replaced instead of adding, neither shape would work: a second request would silently forfeit whatever the first had not yet delivered, and the two sides would disagree about how many values were still owed. ## What the producer may and may not do | Consumer signals | Outstanding demand becomes | The producer may then | |---|---|---| | nothing yet | 0 | deliver no values at all | | request 4 | 4 | deliver up to 4 values | | request 3 with 4 still outstanding | 7 | deliver up to 7 values | | (producer delivers 2) | 5 | deliver up to 5 more | | request an unbounded count | effectively unlimited | deliver everything, as fast as it can | Two asymmetries sit inside that table and both are asked about. 1. **Demand is a ceiling, not an obligation.** With five outstanding the producer may deliver five, two, or none. A source that has nothing to say right now stays quiet with demand outstanding, and that is the normal resting state of an event stream, not a fault. 2. **Demand governs values only.** The signal that terminates the sequence — ordinary completion, or a failure — is not charged against the counter. A producer holding zero demand can still tell the consumer that the sequence is over, which is why an idle, fully-drained pipeline can still shut down cleanly. A request for zero or a negative count is not a polite no-op. The published Reactive Streams specification defines it as a protocol violation and requires the sequence to terminate with an error, precisely because silently ignoring it would leave the two sides holding different beliefs about the outstanding count — and every subsequent decision on both sides is derived from that number. ## Unbounded demand is the documented opt-out A consumer may request an effectively unlimited count. That is a legal move, and it means exactly one thing: from then on there is **no flow control** between that consumer and its source. The producer runs at whatever speed it can, and any protection has to come from somewhere else in the design. It is a decision, not an accident, and it should read like one in the code. ## Keeping the counter honest Request signals arrive from the consumer's side while the producer may be in the middle of delivering. Implementations therefore treat the counter as shared state: the increment from a new request and the decrement from a delivery are applied atomically, and only one drain loop is allowed to be emitting at a time — whichever worker finds the loop free takes it, emits while demand lasts, and releases it, while any other worker just adds its contribution and leaves. Get that wrong and the count drifts, which shows up as a stream that stops early or one that quietly exceeds what was asked for. ## What this buys over a firehose of callbacks A source that simply invokes a consumer callback whenever it has something gives the consumer no vocabulary for *not yet*. The consumer's only levers are to keep up, to queue, or to throw work away. The outstanding count **is** that missing vocabulary: it turns a stream into something the slow end can steer, and it is the single difference between a stream and an unrestrained sequence of callbacks.

  • A consumer has 10 outstanding and the producer holds only 3 values — what is the producer required to do?
    Nothing in particular. It may emit the three and keep seven outstanding for whenever more values exist, emit none for now, or terminate the sequence. Demand is a ceiling on deliveries, never a promise that they will happen, so a source idling with demand outstanding is behaving correctly.
  • What happens if the consumer signals a request for zero values?
    It is a protocol violation rather than a pause. The published specification for demand-driven streams requires the producer to terminate the sequence with an error instead of ignoring it, because a silently dropped request would leave consumer and producer disagreeing about the outstanding count from then on.
  • Does the outstanding count keep growing without limit if a consumer keeps requesting?
    It saturates. Once the accumulated count reaches the effectively-unbounded value the implementation uses, further requests change nothing — the producer is already permitted to send everything. Reaching that point by accident, through repeated large requests, is how a pipeline loses flow control without anyone deciding to remove it.

A request is a signed purchase order for a number of items, not a delivery rate: a second order adds to the first, and the supplier may ship fewer than the total ordered, but never more.

saying these in an interview costs you the question

  • Thinks a later request replaces the earlier one rather than adding to it
  • Describes demand as a rate, such as values per second
  • Believes the producer is obliged to deliver exactly the number requested
  • Assumes exceeding demand is harmless because something downstream will buffer
  • Claims the completion signal must also be paid for with outstanding demand
  • Treats a request of zero as a valid way to pause the stream