You already have futures and promises for asynchronous results. When is a future the wrong abstraction and an asynchronous stream the right one?
answer
- Future: one outcome, once
- Stream: 0..N items plus terminal signal
- Rate mismatch only exists with many items
- Future of a list = full materialization
- Subscription and cancel vs completion handle
basics
~20 sA future carries exactly one outcome, once. A stream carries zero to many items over time plus a terminal completion or failure. Use a stream when results arrive incrementally, when there may be many or infinitely many, or when the producer's rate could outpace the consumer.
solid answer
~60 sA future is single assignment: one value or one error, delivered once, and then it is finished. That makes it perfect for a request and its response, and useless for anything that keeps producing. Reach for a stream when any of these is true: - **Cardinality is not one.** Query results in pages, records from a file, events from a subscription, ticks that never stop. - **You want incremental processing.** A future of a big list means the entire list must be materialized in memory before anyone sees the first element; a stream lets stage one work while the source is still producing. - **Rate matters.** A future has no rate dimension — one hand-off can never be too fast. A sequence can, so a stream carries flow control the future has no need for. A stream also has a richer lifecycle: a subscription that can be cancelled, a terminal signal distinguishing normal completion from failure, and possibly no end at all. If cardinality really is one, prefer the future — it is simpler, cheaper and easier to debug.
go deeper
State the cardinality difference clearly — one outcome versus many over time — and give an everyday example of each.
Add the consequences: incremental processing, bounded memory versus full materialization, the subscription lifecycle and the terminal signal.
Argue about where the boundary sits in a real API, including the conversions and the readability cost of choosing a stream where a future would do.
Treat it as a contract decision across services: what streaming implies for client complexity, cancellation semantics and memory behaviour under load, versus the simplicity of a single-response call.
## The single distinguishing property: cardinality over time A **future** models *one* asynchronous outcome. It starts pending and moves exactly once into a terminal state — a value or an error. It has no notion of the second value, because there is no second value. An **asynchronous stream** models *a sequence* of asynchronous outcomes: zero or more items, followed by at most one terminal signal that says either *finished normally* or *failed*. Everything else that differs between the two abstractions follows from that. ## What follows from the cardinality difference **1. There is a rate dimension.** With exactly one hand-off, the producer cannot be *too fast*: there is one value and one delivery. As soon as a producer can emit repeatedly, its rate can exceed the consumer's, and the excess has to be handled. That is why streams carry flow control machinery and futures do not — not because stream libraries are more thorough, but because the problem does not exist in the single-value case. **2. There is a lifecycle to manage.** A future needs a completion handle. A stream needs a **subscription**: something the consumer holds to signal interest, express demand, and cancel. Cancellation is far more consequential here, because a stream can still be producing when the consumer walks away, whereas a future is a single event that has either happened or not. **3. Completion becomes a separate signal.** For a future, having a value *is* completion. For a stream, the last item and *the end of the stream* are different events, and a consumer often needs to know the difference — to close a file, flush a batch, or emit a total. **4. Memory profile.** A future of a collection forces materialization: the whole collection exists before the consumer sees anything. A stream of the same data can be processed incrementally, with only a bounded window in memory. For a large export or a scan of a big table, that difference decides whether the service survives. **5. Latency to first result.** With a future you wait for everything; with a stream the first element is usable immediately. This is what makes streaming attractive for user-facing progressive rendering and for pipelines whose stages can overlap. ## Concrete signals that you should be using a stream - Results are paginated or chunked, and today you loop, accumulate into a list and return a future of that list. - The source is a subscription or a server push: notifications, change feeds, telemetry, chat messages. - The source has no end: sensor readings, market data, a tailed log. - You want to start processing before the source is finished, or to stop early after finding what you need. - The producer's rate is independent of your consumption rate and you care about bounded memory. ## Signals that a future is the right answer - Exactly one result, known in advance: a lookup, a command, an RPC. - A small collection you genuinely want in one piece, where the whole thing comfortably fits in memory and the caller wants it as a unit. - Code that should stay simple. Streams add operators, subscription lifetimes, cancellation semantics and flow control, all of which cost readability and debuggability. Do not pay for them where cardinality is one. ## The conversions between them The two abstractions interconvert, and knowing the conversions shows fluency: - **Stream to future:** collect the items into a value, or take the first item, or reduce to an aggregate. Note that collecting reintroduces full materialization and its memory cost. - **Future to stream:** a stream that emits one item and then completes. - **Future of a stream:** common when opening the source is itself asynchronous — connect, then stream. A useful mental model: a future is a stream restricted to exactly one item, with the flow-control and lifecycle machinery removed because that restriction makes it unnecessary. ## In an interview Say it in one line — one outcome versus many over time plus a terminal signal — then give the three consequences that matter operationally: incremental processing, bounded memory instead of full materialization, and the existence of a rate mismatch that only appears once there is more than one item. Add that you would keep futures where cardinality is genuinely one, because the simpler abstraction is easier to reason about.
- What is wrong with returning a future of a large list instead of a stream?The entire list must be built in memory before the caller sees any of it, so peak memory scales with the result size and the time to first result equals the time to last result. A stream lets the consumer process items as they arrive with only a bounded window resident, and lets it stop early. The future version also gives the caller no way to slow the producer, since flow control has nothing to act on.
- Can you convert between the two abstractions?Yes, in both directions. A stream becomes a future by collecting its items, taking the first, or reducing to an aggregate — with collecting reintroducing full materialization. A future becomes a single-item stream that completes immediately after emitting. A common shape is a future of a stream, where opening the source is itself asynchronous.
A future is a parcel: it arrives once and the transaction is over. A stream is a conveyor belt: it keeps delivering, you can ask it to slow down, and someone has to say when the belt stops.
saying these in an interview costs you the question
- Believing a future can deliver several values if you complete it repeatedly
- Reaching for a stream when there is exactly one result, paying operator and lifecycle complexity for nothing
- Not noticing that a future of a collection materializes the whole result in memory before anyone sees it
- Assuming flow control is missing from futures by oversight rather than because a single hand-off has no rate
- Forgetting that a stream needs an explicit end-of-stream signal distinct from its last item