How do you spot a per-item lookup hidden in a helper when the calling code reads as plain business logic?
answer
- the call site is not evidence
- follow the callee, not the name
- what runs per element here
- is this already in memory
- size the collection before judging
basics
~20 sAsk what runs per element, not what the line says. Follow every helper called inside an iteration down to whether it reads storage or touches a link the caller never loaded, and size the collection it is called over.
solid answer
~40 sThe call site is useless as evidence — a helper invoked once per item shows one line and no query text. Two habits catch it. First, follow the callee: for every function called inside a loop or a mapping step, read it down to the point where it either reads storage directly or touches a link the caller never loaded; a helper that takes an identifier and returns a whole object is the loudest signal. Second, size the iteration: ask what the largest collection this code can be called over is in real data, since a helper that is fine over three items is not fine over three thousand. Reviewing the diff alone will not do it, because the expensive helper is usually unchanged code that a new list-shaped caller has just started using.
go deeper
Learn the habit of opening every function a loop calls. A one-line call can be a storage read, and the name of the function will usually not tell you.
Explain which signatures imply a read — an identifier in, an object out — and why the count equals the size of the collection the helper is called over rather than anything visible in the code.
Demonstrate a repeatable review move: enumerate what runs per element, resolve each callee, and bound the collection. Note that the culprit is often unchanged code with a newly list-shaped caller.
Argue for making storage access visible by convention — placement, naming, or an explicit accessor parameter — so the property a reviewer needs is legible at the call site instead of two frames down.
The per-item lookup buried in a helper is the hardest of the query-per-row origins to see, because every individual line involved is reasonable. The loop is a normal loop. The helper has a domain name and a domain-shaped signature. The call site is one line long. Nothing in the diff says "storage". Yet the code emits one statement per element, and it usually ships. ## Why review misses it Review looks at a diff, and reads it line by line at the level the names suggest. Three properties of this defect defeat that: - **The cost is not at the call site.** One call, one line, no query text. Everything expensive is one or more frames down. - **The multiplier is not in the diff either.** How many times the helper runs is decided by the caller's collection size, which is a runtime property of the data, not a visible constant. - **The helper is often untouched code.** The genuinely new thing may be a caller that now iterates, using a helper written years ago for single-item use. The defect is in the diff only as a call that looks harmless. ## What to actually check 1. **Enumerate what runs per element.** For every iteration, mapping step, or comprehension in the change, list the calls in its body and resolve each to a concrete implementation. Stop only when you reach code that provably does no storage access. 2. **Look for the shapes that imply a lookup.** A function taking an identifier and returning a whole object almost certainly reads. So does one that takes a partly-populated object and returns an enriched one, and one whose name is a noun-phrase getter for something the caller never fetched. 3. **Ask what the caller loaded.** A helper reading a link that the caller's own read did not include has to fill it. The question to hold in mind is not "does this helper query?" but "is this helper reading something that is already in memory?" 4. **Size the iteration.** Establish the realistic upper bound of the collection: unbounded list, page-capped, or genuinely a handful. A per-item lookup over a bounded handful may be a deliberate and correct trade; over an unbounded list it is a defect regardless of how it reads. 5. **Check the layers the objects cross.** If loaded objects with unfilled links are handed to code that maps or renders them, that code becomes an emitter without any helper being involved at all. ## The two signals ranked | Signal | Strength | Why | |---|---|---| | Called in an iteration and reads a link the caller never loaded | strongest | Both conditions of the defect are present, and the count is the collection size | | Takes an identifier, returns an object | strong | Nothing else can satisfy the signature without a read | | Enriches an object it was handed | moderate | The enrichment source may or may not be in memory already | | Returns a large object rather than a small value | weak | Object size says nothing about where the data came from | | Longer than the loop that calls it | none | Length is not evidence of storage access at all | ## Making the check cheap to repeat Human attention will not hold this for every review, so most teams make some part of it mechanical. Common approaches, all imperfect: run the exercising test with statement logging on and read the log rather than the code; require that functions performing storage access be recognisable — by placement, by naming convention, or by taking an explicit accessor parameter, so an unmarked helper in a loop is visibly wrong; or forbid handing objects with unfilled links across a layer boundary, so downstream code physically cannot become an emitter. The naming and placement conventions are the cheapest, because they turn an invisible property into something a reader can see at the call site — which was the missing information all along. A useful review question to standardise on, because it is short enough to actually get asked: **"What is the biggest collection this loop can run over, and what does the body of it touch that the caller did not already read?"** Both halves are needed. The first without the second flags every loop; the second without the first flags reads that are perfectly fine over three elements. ## The judgment it tests What is being probed is whether you review at the level of *behaviour under real data* rather than at the level of *lines added*. Candidates who have carried this defect to production stop trusting names and start resolving callees; those who have not tend to answer that they "look for loops with queries in them", which is exactly the case that never survives to production because it is the one review already catches.
- The helper itself is unchanged in the diff. Why is it still where the defect lives?Because the defect is the pairing, not the helper. A single-item lookup is correct on its own; it becomes a per-row emitter when a new caller runs it inside an iteration. Review has to weigh unchanged callees against the new caller's shape, which reading the diff alone will not surface.
- How can a helper be made visibly safe or unsafe at its call site?Make storage access part of the signature or the placement: require such functions to take an explicit accessor or live in a layer whose name says so, or return a type that says the value was read. Any convention works if it lets a reader see, without navigating, that the line touches storage.
- When is a per-item lookup inside a loop acceptable rather than a defect?When the collection is genuinely and provably bounded to a handful, and the alternative would complicate the read path for no measurable gain. State the bound explicitly and place it where a future caller will see it, since the whole failure mode is a new caller running the same code over an unbounded list.
saying these in an interview costs you the question
- Judges a call site by its name instead of resolving the callee
- Only looks for loops that contain visible query text
- Ignores unchanged helpers because they are not in the diff
- Never asks how large the iterated collection can get
- Treats returning a big object as proof that it queried