skip to content

A candidate filter combines a cheap stored-field test with an expensive computed-score test using 'and' - what does the order you combine them in decide?

level: seniorimportance: should knowfreq 44%

answer

  1. the first decisive part ends it
  2. cost multiplied by how often it runs
  3. selectivity, not just cheapness
  4. cheap-first inverts for a disjunction
  5. a guard is not an optimisation

basics

~20 s

It decides how often the expensive test runs. A conjunction is settled by the first part that fails, so a cheap, highly rejecting test placed first keeps the score computation off most candidates - the accepted set is the same either way, the work is not.

solid answer

~50 s

Assembling a condition with a conjunction combinator fixes the order its parts are consulted, and a conjunction is decided by the first part that answers no. Put the cheap stored-field test first and the expensive score is computed only for the candidates that survived it: over 10,000 applicants with a cheap test that keeps 5%, that is 500 score computations instead of 10,000. The rule of thumb for a conjunction is cheap-and-highly-rejecting first; for a disjunction it inverts, because a disjunction is settled by the first part that answers yes, so you lead with the cheap test most likely to hold. The result set is unchanged by reordering only when every part is pure and answers for every candidate; once one part guards another, or has a side effect, the order is load-bearing rather than an optimisation.

code

pseudocode · 8 lines
pseudocode
// order is fixed here, once, for every candidate
employable = all([
  hasUploadedProfile,   // guard: must precede anything reading the profile
  hasWorkPermit,        // cheap, rejects most
  scoresAboveThreshold  // expensive, runs only on survivors
])

shortlist = filter(applicants, employable)

go deeper

for a junior

Know that a combined 'and' condition stops as soon as one part says no, so the part you put first is the one that runs for every candidate.

for a middle

Explain the arithmetic: cost per evaluation times how often it runs, with selectivity deciding the second factor. Show the inverted rule for a disjunction.

for a senior

Bring the failure you have seen: a reorder that moved a test past the guard it depended on, or an ordering tuned on last year's rejection rates that stopped paying when the data shifted.

for a principal

Decide how ordering is governed - whether predicates declare cost and selectivity as data, whether guards are marked as immovable, and when the right answer is to push the selective test into the query instead.

## What the combinator fixes When you write `all(hasWorkPermit, scoresAboveThreshold)`, you are not only saying which tests must hold - you are also fixing the sequence in which they will be consulted for every candidate the assembled predicate ever sees. A conjunction reaches its answer as soon as one part answers no; a disjunction as soon as one part answers yes. So the order chosen at assembly time is the order the cost falls in. ## The cost arithmetic Take 10,000 applicants, a stored-field test that reads one already-loaded attribute, and a scoring test that compares a parsed profile against the job description. | Order | Cheap test runs | Expensive test runs | |---|---|---| | cheap first, keeping 5% | 10,000 | 500 | | expensive first | 10,000 | 10,000 | The expensive test runs **20x** fewer times in the first row, and the accepted set is byte-for-byte the same. Two properties drive this, and both matter: - **Cost per evaluation** - how much work the part does when it runs. - **Selectivity** - the fraction of candidates it rejects. A cheap test that rejects almost nobody saves nothing by going first. The heuristic for a conjunction is therefore **cheap and highly rejecting first**, and it inverts for a disjunction: there, the whole condition is settled by the first part that holds, so you lead with the cheap part most likely to **hold**. A team that memorises "cheap first" without the direction will pessimise every disjunction in the codebase. ## When the order is not free to change Reordering is a safe optimisation only when every part is **pure** (it changes nothing observable and answers the same way each time) and **total** (it produces an answer for every candidate it can be handed). Three situations break that: 1. **One part guards another.** If the score test reads a parsed profile that only exists once the earlier test has confirmed the candidate uploaded one, the earlier test is not an optimisation - it is a precondition, and swapping the two turns a filter into a failure. 2. **A part is not pure.** A test that records an audit entry, increments a counter or warms a cache produces different observable output depending on how often it runs, so the order becomes part of the behaviour. 3. **A part can fail rather than answer.** A test that raises on malformed input does not answer no - it stops the traversal - so which candidates get that far depends on what was checked first. A good answer names the guard case explicitly, because it is the one that turns a performance question into a correctness question. ## Making the order explicit rather than accidental - **Order the list deliberately, and say why in a comment or a name.** `all(cheapGuards + expensiveChecks)` documents intent that a hand-written chain hides. - **Where the parts are assembled at run time, sort them.** If each predicate carries an estimated cost or an observed rejection rate, the assembling code can order them instead of trusting the order the criteria happened to arrive in. - **Measure before trusting selectivity estimates.** Rejection rates drift with the data - a test that rejected 95% of applicants last year may reject 20% after a sourcing change, which quietly undoes the ordering. - **Keep guards out of the optimisation conversation.** Mark the parts whose position is required, so a later reorder does not treat them as interchangeable. ## What this is not It is not a claim that fewer evaluations always means a faster filter. Where the expensive test is expensive because it fetches data in bulk, testing it for 500 candidates one at a time can lose to computing it for all 10,000 in a single pass; the ordering heuristic assumes per-candidate cost, and a batched implementation breaks that assumption. It is also not a claim that reordering is a substitute for not doing the work: if the cheap test is selective enough to be an index or a query condition, the strongest move is to stop bringing the rejected candidates into memory in the first place, and let the combinator handle only what is left.

  • When is the order of parts load-bearing rather than a tuning choice?
    When an earlier part is a precondition for a later one - it confirms the data the later test reads exists or is well-formed - or when a part is impure or can fail instead of answering. In those cases swapping the parts changes behaviour or crashes, so the position is part of the filter's meaning and should be documented as such.
  • How does the heuristic change for a disjunction?
    It inverts. A disjunction is settled by the first part that holds, so the work-saving order leads with the cheap part most likely to hold, not the one most likely to reject. Applying the conjunction rule to a disjunction reliably picks the worst order.
  • The criteria are chosen by a recruiter at run time, so you cannot hand-order them. What then?
    Attach a cost estimate and an observed rejection rate to each named predicate, and have the assembling code sort the list before folding it into one condition. That turns ordering into data you can measure and revise, rather than an accident of the order the checkboxes were declared in.

saying these in an interview costs you the question

  • Says reordering changes which candidates pass
  • Applies cheap-first to a disjunction without inverting it
  • Ranks parts by cost alone and ignores how many each rejects
  • Reorders past a part the later test depends on
  • Treats a test that writes an audit record as freely movable
  • Assumes fewer evaluations is always faster, even for batched work