skip to content

Two issuers built from one factory are numbering into a single shared sequence instead of independently: what is wrong with where the counter was declared?

level: seniorimportance: should knowfreq 38%

answer

  1. one sequence under two prefixes
  2. distinct functions, one binding
  3. count entries of the declaring scope
  4. declared outside means created once
  5. prove it by advancing one, reading the other

basics

~20 s

The counter was declared in the scope enclosing the factory rather than inside its body. That scope is entered once, so one binding exists and every issuer the factory returns captured the same one. Moving the declaration inside the body gives each call its own.

solid answer

~40 s

A binding is created when the scope declaring it is **entered**, not when the factory is written. Declared inside the factory body, each call creates a fresh counter and each returned issuer captures its own. Declared in the enclosing scope, that scope was entered once, so there is exactly one counter and every issuer ever returned advances it — which is why two streams interleave on one sequence. The same symptom appears if the factory builds its issuer once and hands the same function value to every caller; then there is one closure, not two. Diagnose it by counting how many times the declaring scope is entered, and confirm the fix by advancing one issuer several times and checking the other still starts at one.

code

pseudocode · 22 lines
pseudocode
counter = 0                        # DECLARED OUTSIDE - this scope is entered once

function makeIssuer(prefix):
    function next():
        counter = counter + 1
        return prefix + counter
    return next

a = makeIssuer("A-")
b = makeIssuer("B-")

a()      # A-1
b()      # B-2   <- shares a's counter, not B-1
a()      # A-3

# fix: move the declaration inside, so entering the body creates it
function makeIssuer(prefix):
    counter = 0
    function next():
        counter = counter + 1
        return prefix + counter
    return next

go deeper

for a junior

Recall the rule that decides it: a variable is created when its declaring scope is entered, so one declared outside the factory exists once and is shared by everything that captured it.

for a middle

Explain why the symptom is deceptive — two different function values, one binding — and where the declaration must go so that each factory call creates its own state.

for a senior

Diagnose it the way you would in production: unique identifiers but an interleaved sequence, count the entries of the declaring scope, check whether the factory is handing back a cached issuer, and prove the fix behaviourally rather than by inspection.

for a principal

Decide when a single shared sequence is actually the requirement and make that explicit in the design, so the next reader is not left inferring intent from where a declaration happened to sit.

## The symptom Two batch streams each asked the factory for an issuer, expecting `A-1, A-2, A-3` and `B-1, B-2, B-3`. What the logs show is `A-1, B-2, A-3, B-4`: two prefixes, one sequence running underneath them. Nothing throws, nothing is slow, and every identifier is still unique — which is why this survives review and is found in production, usually when somebody tries to count the items in a stream by reading its last number. ## The rule that explains it One sentence decides the whole question: **a binding is created when the scope that declares it is entered, not when the program text is written.** Everything follows from counting entries. - The counter declared **inside the factory body**: that body is entered once per factory call, so two calls create two counters, and each returned issuer captured a different one. - The counter declared **in the scope enclosing the factory**: that scope is entered once, so there is exactly one counter, and every issuer the factory has ever returned captured that same binding. The returned functions are different function values in both cases — which is what makes the bug so deceptive. Two distinct issuers can perfectly well share one captured binding; distinct functions do not imply distinct state. ## Reading the scope, in order 1. **Find the declaration of the state**, not its uses. Then ask which enclosing construct it belongs to. 2. **Count how many times that construct is entered** over the program's life. Once at start-up? Then there is exactly one binding, no matter how many issuers exist. 3. **Count how many issuers captured it.** If that number exceeds the number of entries, they are sharing. 4. **Check the factory itself for reuse.** If the factory computes its issuer once and returns the same function value to every caller, the count of closures is one and step 2 will not show it. ## The fix, and how to prove it Move the declaration inside the factory body so that entering the body creates the state. Then prove it behaviourally rather than by reading: advance the first issuer several times, then take the first value from the second issuer. It must be the initial value. Asserting that two issuers are different values proves nothing, since two different functions may share one binding — the test has to observe the state, not the function identity. | Where the state is declared | Entries of that scope | Bindings | What the issuers see | |---|---|---|---| | Enclosing the factory | once | one | one shared sequence | | Inside the factory body | once per factory call | one per call | an independent sequence each | | Inside the returned function | once per issued number | one per call, discarded | never advances past the first value | The third row is the over-correction, and it is worth knowing because it is what a hurried fix produces: pushed all the way inside the inner function, the counter is re-created on every call and every identifier comes out as number one. The state must be declared in the scope whose lifetime you want it to have — which here is the issuer's. ## The other ways one binding ends up shared - **A cached factory.** The factory memoises its result and returns the same issuer to every caller. The declaration is correct; the number of constructions is not. - **Shared derived state.** The counter is per-call but a second captured thing — a list of issued identifiers, say — was declared outside and is appended to by every issuer. - **A captured mutable structure.** Each issuer captured its own binding, but all those bindings refer to one mutable holder created once, so the mutation is shared through the contents rather than the binding. All three have the same shape: **one thing where the design assumed one per issuer.** That is the question to ask of any closure-held state, and it is worth asking in the other direction too, because sometimes sharing is the intent. A single sequence across all streams is a perfectly reasonable requirement; the defect is only that the code expresses one thing while the design assumed the other. Where sharing is intended, declare the state once, deliberately, and say so — do not leave a reader to infer it from a scope. ## The concurrency caveat Separating the bindings fixes the interleaving between streams; it does not make either issuer safe under concurrent use. One issuer advanced from several workers at once is still a read-modify-write on one variable, and coordinating that is a different problem from the one diagnosed here. Say so explicitly rather than implying that per-issuer state is per-worker safety. ## What to say in the interview "Distinct closures do not imply distinct state. A binding exists once per entry of its declaring scope, so a counter declared outside the factory is one binding shared by every issuer. Move it inside the body, then prove it by advancing one issuer and checking the other still starts at the beginning."

  • A colleague fixes this by declaring the counter inside the returned function instead. What happens?
    Every identifier becomes number one. That scope is entered once per call and the binding is discarded when the call returns, so nothing survives to be advanced. The state must be declared in the scope whose lifetime you want it to have — here the issuer's, which is the factory body.
  • Which test actually proves the two issuers are independent?
    Advance the first several times, then take the first value from the second and assert it is the initial one. Asserting that the two issuer values are not identical proves nothing, because two distinct function values can capture the same binding.
  • The declaration is inside the factory, yet both issuers still share a sequence. Where do you look next?
    At how many times the factory actually ran. A factory that caches and returns one issuer to every caller produces a single closure, so the declaration is irrelevant. Failing that, look for a captured mutable holder created once and referred to by every issuer.

saying these in an interview costs you the question

  • Says two distinct function values must have distinct captured state.
  • Moves the declaration into the inner function and calls the sharing fixed.
  • Blames concurrency for a sequence that interleaves in single-threaded runs.
  • Claims each capture takes its own private copy of an enclosing variable.
  • Tests independence by asserting the two issuer values are not the same object.