A back-off helper can cap its delay sequence at ten entries or hand back an endless one - what does each choice give the caller?
answer
- where does the stopping decision live
- one policy against many policies
- the signature hides the hazard
- count, predicate or deadline bounds
- publish the rule plus one bounded convenience
basics
~20 sCapping inside the producer makes every consumer safe but fixes one policy for all of them. An endless producer leaves stopping to the consumer, who can bound by count, by predicate or by deadline - or forget to bound at all.
solid answer
~40 sThe question is where the **stopping decision** lives. A capped producer owns it: every caller is safe, no caller can hang, and no caller can want an eleventh delay either - the policy is baked in for all of them. An endless producer hands the decision out: one caller takes three attempts, another keeps going while the delay stays under a ceiling, a third stops at a deadline, all from one definition, with the bound written where the knowledge of the budget actually lives. The cost is an obligation the caller must remember, and a hazard the signature does not show - a sequence looks the same whether it ends or not. The usual resolution is to publish both: the rule for callers with their own policy, and one bounded convenience for everyone else.
code
pseudocode · 12 lines// producer decides: safe for everyone, same policy for everyone
function backoff_capped(start, factor, how_many):
return take(sequence(start, function(d) return d * factor), how_many)
// consumer decides: one rule, every caller bounds it
function backoff(start, factor):
return sequence(start, function(d) return d * factor)
by_attempts = take(backoff(100, 2), 3)
by_ceiling = take_while(backoff(100, 2), function(d) return d < 5000)
// by_attempts: 100, 200, 400
// by_ceiling: 100, 200, 400, 800, 1600, 3200go deeper
Know the difference in plain terms: one call hands back a fixed number of delays, the other hands back a rule that keeps going, and only the second makes stopping your job.
Argue both sides - reuse and per-caller policy against an obligation the caller must remember - and name the three bound shapes a consumer has: a count, a predicate, or a deadline.
Say what you would actually publish in a shared codebase, how you would name it, and how you keep an unbounded producer from reaching code that aggregates over whatever it is given.
Own the trade-off across teams: generality in a shared library against a failure that surfaces in someone else's service, and whether the standard should be bounded-by-default with the rule available on request.
## Two places the stopping decision can live Both versions compute the same delays in the same order. The only difference is **who decides when to stop**, and that single difference propagates into reuse, safety, review and the failure mode. - **Producer-decides.** The helper takes the seed and the growth factor, applies its own cap, and returns ten delays. The result is an ordinary finite value. Nothing a caller does with it can hang. - **Consumer-decides.** The helper returns the rule itself - an endless sequence - and every caller bounds it. A prefix count, a predicate, or a budget the caller holds outside the sequence entirely. ## What the caller gains from the unbounded version - **Per-caller policy.** A background reconciler and an interactive request have genuinely different retry budgets; a single cap serves neither well. - **Bounds of different shapes.** Count, predicate and deadline are all expressible against the same producer; a fixed-count return value only supports "take fewer". - **The bound sits where the knowledge is.** The call site knows the deadline it is working against. The shared helper does not, and a helper guessing it is a policy decision hidden in a library. - **No wasted work when the caller stops early.** A capped helper that eagerly computes ten delays has computed ten whether or not the second attempt succeeded. ## What the caller now carries - **An obligation.** Somebody must bound it, on every path, including the one added next quarter. - **An invisible hazard.** A sequence type is the same type whether its producer ends or not, so nothing in the signature warns that a whole-sequence step over it will never return. The name and the documentation are the only warning the caller gets. - **A failure that surfaces somewhere else.** The library is correct, the call site is where the process pins a core, and the two are usually owned by different people. ## Side by side | | producer decides (capped) | consumer decides (endless) | |---|---|---| | who chooses the bound | the library author, once | each call site, per use | | reuse across differing budgets | poor - one policy for all | good - one definition, many bounds | | cost of a caller forgetting | none; there is nothing to forget | a step that never returns | | what the signature tells a reader | a finite result, its size fixed | almost nothing about termination | | changing the policy later | edit the library, affects everyone | edit one call site | | can a caller exceed the policy | no | yes, which may be the point or the danger | ## The middle ground These are not exclusive, and the usual answer in a shared codebase is to publish both: the endless rule for callers with a policy of their own, plus one bounded convenience that covers the common case, named so that the difference is impossible to miss at the call site. What makes this work is the naming, not the availability - if the two are distinguished only by an argument, readers will not notice which one they got. ## How to choose 1. **Whose policy is the bound?** If the limit belongs to the platform - a retry budget, a quota, a safety limit a caller must not be permitted to raise - then cap it in the producer. A bounded result is then the contract, and handing out the rule would let a caller violate it. 2. **Do callers genuinely differ?** If every caller wants the same ten delays, the flexibility of an endless producer buys nothing and costs a hazard. 3. **How far does it travel?** An endless producer created and bounded in the same function is nearly free of risk. One that crosses a module boundary and arrives at code which aggregates over whatever it is handed is how a hang gets built out of two correct halves. ## The claim to resist "The unbounded version is strictly more general, so it is strictly better." Generality is real, but so is the transfer of an obligation from one author who thinks about it once to every caller who must think about it every time. That trade is worth making where callers genuinely have different budgets, and is pure cost where they do not.
- Does an unbounded return value tell a reader of the signature anything about the hazard?Usually very little. A sequence is the same type whether its producer ends or not, so nothing in the shape warns that a whole-sequence step will never return over it. The name, the documentation, or a distinct type reserved for never-ending sources has to carry that warning, because it is the one cost a caller cannot discover from the signature alone.
- When is capping inside the producer clearly the right call?When the bound is policy the producer owns rather than the caller's business - a retry budget the platform sets, a quota, a safety limit nobody downstream may raise. Then the finite result is the contract rather than a convenience, and handing out the endless rule would hand out permission to violate it.
saying these in an interview costs you the question
- Says the endless version is always better because it is more general.
- Assumes callers will obviously remember to bound it.
- Thinks a caller can extend a producer that already capped itself.
- Treats the two as equivalent because the first elements match.
- Believes the signature tells the caller which one it received.