skip to content

Two payroll routines both write a module-level running-total ledger — what does that shared state cost you when a third routine joins?

level: seniorimportance: should knowfreq 58%

answer

  1. state nobody declared as a parameter
  2. an argument missing from every signature
  3. call order becomes an unwritten contract
  4. tests must reset the module first
  5. per-run state stored per process

basics

~20 s

Module-level state acts as an argument no signature declares. A third writer couples silently to the other two, turns call order into an unwritten contract, and forces every test, retry and concurrent run to reset the ledger first.

solid answer

~50 s

The ledger is an **implicit parameter**: every routine that touches it takes it and returns it, but no signature says so. Adding a third writer therefore couples three routines through storage a caller cannot see, and makes **call order** part of the contract while leaving it out of every declaration. The costs land in four places — reading (you must search the module to learn who changes the total), testing (no routine can be exercised without arranging and resetting module state, and tests stop being independent), re-entrancy (a retry or a second run in the same process inherits the first one's residue), and change (any edit to the invariant has a blast radius of every routine that reads it). The repair is to make the state explicit: pass a run record through the chain, or funnel all writes through one routine that owns the invariant.

code

pseudocode · 13 lines
pseudocode
// module level: the ledger is an argument no signature mentions
set runningTotal = 0

function addGross(employee)
    set runningTotal = runningTotal + employee.gross

function addDeductions(employee)
    set runningTotal = runningTotal - employee.deductions

// the same state, now visible at every call site
function addGross(run, employee)
    set run.total = run.total + employee.gross
    return run

go deeper

for a junior

Recognise that a variable declared outside every routine can be changed by any of them, and that a routine using one needs more than its arguments to predict what it will do.

for a middle

Explain module-level state as an undeclared parameter, and name the two visible consequences: call order becomes part of the contract, and no routine can be tested without arranging the module first.

for a senior

Bring the failure story — a retry or a second run inheriting a non-zero total — and the repair: thread a per-run record through the chain, or give the invariant exactly one writing routine.

for a principal

Draw the line for the codebase between per-process state that may live at module level and per-run state that may not, and make it reviewable, because this defect is always introduced one locally reasonable line at a time.

## Module-level state is an argument nobody declared A procedural program is subroutines over shared data, so the question is never *whether* routines share state but *where the sharing is written down*. When the running total lives at module level, each routine that touches it effectively has the signature `f(employee, ledger) -> ledger` while presenting itself as `f(employee)`. The data dependency is real; only its declaration is missing. That gap is what the third writer exposes. Two writers can be held in one head. Three cannot, because nothing at a call site distinguishes a routine that reads the ledger, one that writes it, and one that does neither. ## The four costs, named - **Reading.** To answer "what can change the total?" you must search the whole module rather than read the call sites of one routine. Reviews get worse in proportion to module size. - **Ordering.** If `addGross` must run before `addDeductions`, that ordering is now a contract enforced by nothing. A new routine inserted in the middle, or a reordering during a refactor, produces a wrong number rather than a failure. - **Testing.** No routine can be exercised alone: each test must arrange the module's state and reset it afterwards. Tests stop being independent, and a failure in one can be caused by another that ran before it. - **Re-entrancy and concurrency.** A second payroll run in the same process, a retry after a partial failure, or two runs in parallel all inherit or race on one ledger. The single-threaded case is not safe either — a retry that starts from a non-zero total is the classic production incident here. | Property | Explicit run parameter | Module-level ledger | |---|---|---| | Visible at the call site | yes, in the signature | no | | Call order | constrained by data flow you can see | an unwritten convention | | Test setup | construct one value, pass it | arrange and reset module state | | Second concurrent run | another value, no interaction | contention or corruption | | Blast radius of a change | the routines that take it | every routine in the module | ## The maintenance story that follows 1. A third routine needs a running figure and finds the ledger already there. Reusing it is one line; threading a parameter through the chain is ten. It reuses it. 2. The third routine writes the ledger for its own purposes and resets it when it finishes, which is correct in isolation. 3. Someone calls the third routine from inside the run, between the other two. The reset now discards a partial total, and the payroll is wrong by whatever had accumulated. 4. The bug reproduces only when routine three is reached, so it reproduces in production and not in the tests, each of which exercises one routine on a freshly initialised module. Nothing in that sequence involves a careless engineer. Each step is locally reasonable, which is precisely what makes implicit state expensive. ## Making it explicit, and when not to The repair has two forms, and they combine: - **Thread the state.** Pass a run record through the chain so each routine's dependency is in its signature. This costs parameter plumbing and is worth it wherever the state is per-run rather than per-process. - **Give the invariant one owner.** If the state must stay at module level, let exactly one routine write it and make every other routine go through that one. You keep the convenience and regain a single place to read, to log and to assert the invariant. Module-level state is still the right answer in narrow cases: configuration read once and thereafter only read, a memoisation cache whose contents cannot change the result, a counter whose exact value is not part of any output. The test is whether the state is **per run** or **per process**. A running total is per run, and per-run state stored per process is the shape of this whole class of defect.

  • The team says threading a run record through eight routines is pure ceremony. What is the counter-argument?
    The dependency exists either way; the only question is whether a reader can see it. Threading it makes call order follow visible data flow, lets any routine be tested with a constructed value, and allows two runs to coexist. If the plumbing is genuinely painful, that pain is a signal the chain is too deep, which is a decomposition problem worth its own fix.
  • When is module-level state still the right call in a procedural program?
    When the state is per process rather than per run and nothing in an output depends on when it changed: configuration loaded once and thereafter only read, a cache whose contents cannot alter a result, a diagnostic counter. A running total fails that test, because it is per run and its value is the output.
  • The state must stay at module level. What is the cheapest thing that regains most of the safety?
    Give it exactly one writer. Every other routine calls that one, so the invariant has a single place where it is established, logged and asserted, and a reader has one routine to check rather than a module to search. Reads may stay direct; it is the scattered writes that cost you.

A whiteboard tally in a shared office: anyone may add to it and everyone can see the current number, but when it comes out wrong there is no record of who wrote last or in what order.

saying these in an interview costs you the question

  • Claims narrowing visibility to the module removes the ordering contract
  • Says a single-threaded batch job cannot be hurt by shared module state
  • Treats passing the state explicitly as ceremony with no benefit
  • Believes tests are independent while they share module-level state
  • Assumes the last writer is obvious from reading any one routine