skip to content

Which parts of a job's logic cannot be lifted into a plain function, and what do you do about them?

level: middleimportance: should knowfreq 46%

answer

  1. rules lift, arrangement does not
  2. fragments out, composition stays
  3. merge is a function, its schedule is not
  4. state as old plus record to new
  5. keep business rules out of wiring

basics

~20 s

Compositions do not lift: which records land in the same group, how many times a partial merge runs and where, what a join does with an unmatched key. The fragments inside them do lift, so keep the wiring free of business rules and test the fragments exhaustively.

solid answer

~40 s

Per-record rules lift cleanly. What does not is the composition the runtime performs around them: the grouping that decides which records meet, the number of times a partial merge is applied and on which worker, the order pieces of the input arrive in, the treatment of a key with no match on the other side of a declared join. The fragments inside those compositions are still ordinary functions - the record-to-key mapping, the merge of two partial values, and for a long-running step the transition from one retained set and a record to the next retained set and its outputs. Lift and test all three. Then keep the wiring free of business conditions, so what remains untestable is arrangement rather than meaning.

go deeper

for a junior

Know the two halves: what one record becomes is a function you can call; which records end up together is something the system arranges. Tests reach the first easily and the second not at all.

for a middle

Explain which fragments pull out of a grouped or stateful step - the key mapping, the merge, the state transition - and why the schedule of those calls is a runtime property your test has not exercised.

for a senior

Demonstrate the discipline that keeps the untestable surface from growing: no business condition in a declaration, and a written statement of what the remaining arrangement still needs a real run to prove.

for a principal

Treat the liftable share as a design property of the codebase, not an accident. It decides how much verification a team can afford per change and how quickly a pipeline can be altered safely.

## Rules lift; compositions do not The separation that makes a distributed job testable has a boundary, and knowing where it falls is what distinguishes a candidate who has done this from one who has read about it. **Lifts into a plain function:** - the mapping from a record to the value it is grouped by; - the predicate that keeps or drops a record; - the derivation that turns fields into fields; - the merge of two partial values into one; - for a step that carries a retained set - everything a long-running job still holds between records, such as counters, buffered sides of a join, or the last value per key - the transition function taking the old retained set and one record and returning the new retained set plus whatever it emits; - the shaping of the final output record. **Does not lift, because it is arrangement rather than meaning:** - **which records meet.** Records that must be compared to each other have to be held by the same worker, which means a redistribution: the point where a step needs records currently held by other workers, so every worker writes its records out and every worker fetches the ones addressed to it. Whether your key produces the grouping you intended is a property of that arrangement. - **how many times a merge runs, and where.** Runtimes differ: some apply a partial merge on each worker before records meet, some do not, some decide at run time. The function must tolerate whichever, and a test that calls it once has only proved the single-call case. - **the order pieces arrive in** at the step that combines them, which is not yours to choose. - **what a declared join does** with a key that has no match on the other side, since that behaviour is the runtime's, not your function's. - **whether the retained set you wrote is the one you get back** after a restart, a code change or a change of width. The algebra a partial merge must satisfy for the result to be independent of how the work was split is the parallel-reduction pattern's subject (the parallel-reduction pattern). Here the point is narrower: the merge is a callable function, while the schedule of its applications is not. ## The wiring wants to be boring The untestable surface is not fixed - it grows every time a business rule is typed into the wiring. Three common leaks: 1. A filter condition written inside a declared join instead of as a predicate. 2. A special case for one customer expressed as an extra branch in an output declaration. 3. A cutoff date typed into a grouping or partitioning declaration rather than passed in. Each of those can only be exercised by running the job. Move them out and the wiring reduces to locations, declarations and references to lifted functions - arrangement with no meaning in it. ## Making a stateful step liftable A long-running step is the case people give up on, and it is the most rewarding one. Write the logic as a pure transition: - input: the retained set as a plain value, plus one record; - output: the new retained set, plus zero or more emitted records. That function is testable as a *sequence*: feed it ten records in order and assert on both the emissions and the retained set at each point, including the ones that should expire something. What the test still cannot prove is that the runtime hands you the retained set you last returned - after a restart, after a width change, after a code change - because that is the runtime's contract and it differs between them. ## The residue, stated honestly | fragment | liftable | what still needs a run | |---|---|---| | record to grouping key | yes | whether that key spreads records the way you assumed | | predicate, derivation | yes | nothing, if it is genuinely per-record | | merge of two partials | yes | how many times it is applied and on which worker | | retained-set transition | yes | that the runtime returns the set you wrote, across restarts | | join semantics for a missing match | no | the declared behaviour, on real inputs | The answer an interviewer is listening for has both halves: the list of fragments you pulled out and tested, and the short, specific list of what is left - not a shrug about integration testing.

  • How do you make a step that carries a retained set testable as a plain function?
    Express it as a transition: old retained set plus one record in, new retained set plus emitted records out. Then a test drives a sequence of records through it and asserts on both the emissions and the set after each one, including expiry. What stays unproven is the runtime's side of the contract - that the set you returned is the one handed back after a restart or a change of width.
  • If the wiring should hold no business rules, what is legitimately left in it?
    Input and output locations, the declarations of grouping and joining, the output mode, parallelism and resource choices, and references to the lifted functions. It is arrangement with no meaning in it. A useful review question is whether a reader could change a business rule without touching the wiring at all; when the answer is no, something has leaked in.

saying these in an interview costs you the question

  • Says grouped or stateful steps simply cannot be tested at all.
  • Puts business conditions inside declared joins and then calls the wiring untestable.
  • Assumes the per-key merge is called exactly once per key on every runtime.
  • Treats a green test of the merge function as proof the grouping is right.
  • Keeps a cutoff date in a grouping declaration rather than passing it in.