skip to content

A batch job's sequence issuer keeps its counter in a variable captured by the returned function: why does that counter survive between calls?

level: juniorimportance: must knowfreq 66%

answer

  1. state that survives between calls
  2. lifetime follows the function value
  3. the frame does not end the binding
  4. no name for it outside the body
  5. one counter per factory call

basics

~20 s

The returned function captured that variable, so the variable lives as long as the function value does instead of ending with the call that created it. No code outside can name it, so the returned function is the only way in.

solid answer

~40 s

The outer function declares `counter` and returns an inner function that refers to it. A closure is a function value bundled with the bindings it referred to where it was written, so the returned issuer carries that binding with it. A local normally disappears when its call returns, but only because nothing outlives the call; here something does, so the binding stays reachable and keeps its value across calls. It is private for a plain scoping reason: the only place the name `counter` exists is the outer body, which has already finished, so no other expression in the program can read or reset it. Calling the factory again enters that body again and creates a second, independent counter.

code

pseudocode · 13 lines
pseudocode
function makeIssuer(prefix, width):
    counter = 0                       # created when this body is entered
    function next():
        counter = counter + 1
        return prefix + padLeft(counter, width)
    return next

issuerA = makeIssuer("RUN-", 4)
issuerB = makeIssuer("RUN-", 4)

issuerA()    # RUN-0001
issuerA()    # RUN-0002
issuerB()    # RUN-0001 - its own counter, unaffected by issuerA

go deeper

for a junior

Recall the one-line rule: the returned function carries the variable it referred to, so the variable lives as long as that function does. Be ready to say why outside code cannot reset it.

for a middle

Explain that a binding is created when its declaring scope is entered, which is why two calls to the factory give two independent counters, and why the privacy is a scoping consequence rather than a keyword.

for a senior

Show you know what this costs and what it hides: state with no name is state no dashboard, test hook or operator can inspect, so say how you would expose a read path deliberately when a running batch needs one.

for a principal

The trade you own is where hidden state is allowed to live at all: captured state is unreachable by design, which is exactly why it resists auditing, so decide when a team should prefer state the caller passes and returns.

## The shape in question A **sequence issuer** is a small piece of a batch job: something has to hand out `RUN-0001`, `RUN-0002`, `RUN-0003`, one identifier per unit of work, never repeating. Written with a closure it is three lines of structure. An outer function declares a variable set to zero, defines an inner function that adds one to that variable and formats the result, and returns the inner function. The outer call then finishes. What the caller holds afterwards is a single function value. The interviewer's real question is: the outer call has returned, so why is the variable still there on the next call, and why can nothing but the returned function touch it? ## The variable's lifetime follows the function, not the call The familiar model says a function's locals live in the frame made for that call and vanish when it returns. That model is a special case — true only while nothing outlives the call. A **closure** is a function value bundled with the bindings it referred to in the scope where it was written: its captured environment. The inner function refers to the counter, so the counter is part of what the returned value carries. So state the ordering plainly: **the counter's lifetime is the issuer's lifetime**, not the factory call's. The factory call ends; the binding it created does not, because something still refers to it. Only when the last reference to the issuer is dropped does the counter become unreachable and eligible to be reclaimed, and exactly when that memory is released differs between runtimes. ## Why this counts as private state Privacy here is not a convention and not a keyword. It is **scope**. The name `counter` exists only inside the factory's body, and that body has finished executing. No expression anywhere else in the program can be written that names it, so no other code can read it, reset it, or write to it. The only channel is the function that was returned, and that function exposes exactly what it does: - it exposes **advance and return the next value**, because that is its whole body; - it does not expose **read without advancing**, unless the factory deliberately returns a second function for that; - it does not expose **set to an arbitrary value**, unless the factory returns something for that too; - it cannot be bypassed by reaching for a field, because there is no named field to reach for. That is the entire interface story of this shape: the surface is exactly the set of functions the factory chose to return, and nothing leaks by accident. Inspection and debugging tooling can of course still display a captured value — that is an inspection channel, not a name the program can use. ## One issuer, one counter The second half of the answer, and the half candidates get wrong, is how many counters exist. A binding is created when the scope that declares it is **entered**, not when the text is written. Each call to the factory enters its body again, so: 1. the first call creates counter A and returns issuer A, which captured A; 2. the second call creates counter B and returns issuer B, which captured B; 3. the two issuers advance independently, and neither can observe the other's value. If the counter were instead declared in the scope that *encloses* the factory, that scope is entered once, one binding exists, and every issuer the factory returns captures the same one — which is how two batch streams end up colliding on identical numbers. ## The three ways to keep a number between calls | Approach | Who can change it | Independent per user | Cost | |---|---|---|---| | Counter in a wide enclosing scope | anything that can name it | no — one shared value | cheapest, least safe | | Counter captured by a returned function | only the returned function(s) | yes — one per factory call | one allocation per issuer | | Number passed in and returned by a pure function | the caller, who must store it | yes — the caller owns it | no hidden state, more caller work | The third row deserves naming because it is the honest alternative: a pure `next(previous)` keeps no state at all and pushes the bookkeeping onto every caller. The closure form is what you reach for when you want that bookkeeping to be somebody's job, and that somebody to be unreachable. ## What to say in the interview Give the three rules in order — lifetime, privacy, count. The returned function captured the binding, so the binding lives as long as the function does. Nothing outside the factory can name it, so the returned function is the only door. And because a fresh binding is created every time the factory body is entered, two issuers never share a number.

  • The factory is called twice. How many counter variables exist, and why?
    Two. A binding is created when its declaring scope is entered, and each call enters the factory body again, so each returned issuer captured a different variable. The two sequences advance independently and neither can read the other's value.
  • You now need to read the current number without advancing it. How do you add that without losing the privacy?
    Have the factory return two functions that close over the same counter — one that advances and returns, one that only reads. Both share the binding, callers still have no name for it, and you have chosen to expose reading while still refusing arbitrary writes.
  • What becomes of the captured counter once nothing holds the issuer any more?
    It becomes unreachable, because the only reference to it was the function value that has now been dropped, and it is then eligible to be reclaimed. When the memory is actually released is a runtime detail rather than a property of closures.

A ticket roll sealed inside a dispenser: every pull gives the next number, the roll keeps its place between customers, and nobody standing outside can reach in and wind it back.

saying these in an interview costs you the question

  • Says the counter resets to zero on every call to the issuer.
  • Claims a value can only persist between calls if it is in a program-wide scope.
  • Thinks the returned function re-runs the whole factory body each time it is called.
  • Assumes every issuer a factory returns necessarily shares one counter.
  • Says the state is private only by naming convention, not by scope.