skip to content

Wrappers from a configuration list are applied left to right to a lookup function — which one ends up innermost?

level: middleimportance: nice to knowfreq 30%

answer

  1. each step wraps what exists so far
  2. first applied is closest to the original
  3. last applied is what callers hold
  4. in through the outermost, out in reverse
  5. reverse the list to read outermost-first

basics

~20 s

The first wrapper applied ends up innermost, closest to the original function, and the last one applied ends up outermost. So a call enters through the last entry in the list and reaches the original only after passing every earlier one.

solid answer

~40 s

Building a stack from a list means starting with the raw function and repeatedly replacing it: `wrapped = w(wrapped)` for each `w` in order. Each step puts the new wrapper **around** everything built so far, so the first entry applied sits nearest the original function and the last entry applied sits outermost. Reading the list `[timing, retry, caching]` left to right therefore produces `caching(retry(timing(lookup)))`: a call is seen by caching first, then retry, then timing, then the lookup, and results come back out in reverse. If you want the list to read outermost-first — the way most people expect a middleware list to read — apply it right to left, or reverse it before folding. Whichever you choose, document it, because the two orders are equally plausible to a reader and behave differently.

code

pseudocode · 7 lines
pseudocode
wrapped = lookup
for each w in [timing, retry, caching]
    wrapped = w(wrapped)

// wrapped is caching(retry(timing(lookup)))
// entering:  caching -> retry -> timing -> lookup
// returning: lookup -> timing -> retry -> caching

go deeper

for a junior

Expand the loop by hand for a two-entry list before answering. Seeing w2(w1(f)) written out settles which wrapper is nearest the original far more reliably than reasoning about the word 'first'.

for a middle

Explain both passes: the entering order is the reverse of the application order, and the returning order reverses it again, so the outermost wrapper brackets everything the others do.

for a senior

Show that you treat the fold direction as an interface decision — that the configuration reads the way operators expect, and that the mapping between list order and stack-trace order is written down.

for a principal

The lead's call is whether a shared wrapper list is a good idea at all: it standardises behaviour across services, and it makes every service's call path depend on an ordering decided elsewhere.

## Building a stack from a list Wrappers are usually not written as one hand-nested expression; they are assembled from a list, so that configuration or a registry decides which are enabled. The standard assembly is a fold over the list starting from the raw function: - start with `wrapped = lookup`; - for each wrapper `w` in the list, set `wrapped = w(wrapped)`. Each step wraps **everything produced so far**, which is the key to the whole question. ## Which end is innermost Take the list `[timing, retry, caching]` and trace it: 1. `wrapped = timing(lookup)` 2. `wrapped = retry(timing(lookup))` 3. `wrapped = caching(retry(timing(lookup)))` The **first entry applied is innermost** — it is closest to the original function and every other wrapper is outside it. The **last entry applied is outermost** — it is the function callers actually hold, and the first thing a call meets. People get this backwards constantly, because 'first in the list' feels like 'first to run', and in one sense it is not: the last entry runs first. The reliable check is to expand two steps of the loop on paper rather than to reason from the word 'first'. ## The two traversals A call makes two passes through the stack, in opposite directions. | Position | On the way in | On the way out | |---|---|---| | last applied (outermost) | sees the call first; may answer without calling inward | touches the result last | | middle | sees only what the outer layer passed down | touches the result second | | first applied (innermost) | sees the call last, immediately before the original | touches the result first | So 'before' code in the outermost wrapper runs before every other wrapper's 'before' code, and its 'after' code runs after every other wrapper's 'after' code. Anything measuring or guarding the whole call belongs outermost; anything that must sit as close to the real work as possible belongs first in the list. ## Making the list read the way people expect Most readers of a configuration list expect the top entry to be the outermost layer — the first thing a request meets — because that is how a request pipeline is usually drawn. A left-to-right fold gives them the opposite. Two ways to fix it: 1. **Reverse the list before folding**, so the written first entry ends up outermost. 2. **Fold from the other end** — walk the list from its last entry to its first, applying each in turn. Both produce the same result and the choice is a readability one. What must not happen is leaving it implicit: the nesting order is behaviour, and a reader who assumes the wrong direction will predict the wrong outcome for exactly the cases that matter — which wrapper short-circuits which, and which one sees a failure first. ## Practical consequences - **Insertion position is not append position.** Adding a wrapper to the end of the list makes it the new outermost layer, ahead of everything already there; adding it to the front buries it next to the original. - **A disabled entry changes the neighbours' positions**, not just its own presence — the wrapper that was two layers out is now one. - **A stack trace reads outermost-first**, so the frame order you see during an incident is the reverse of the order the list was applied in. Knowing which direction the list folds is what lets you map one to the other. - **Wrappers are not generally commutative**, so 'the list contains the right set of wrappers' is never sufficient; the order in it is part of the specification.

  • You append a new wrapper to the end of that configuration list. Where does it end up?
    Outermost, above everything already in the list, so it is the first to see every call and the last to touch every result. If it was meant to sit next to the raw function — an outbound-request logger, say — appending puts it in the wrong place, and it will see calls the layers below it would have answered themselves.
  • Why does a stack trace appear to contradict the list order?
    It does not contradict it, it reads the other end. A trace shows the outermost frame first, which is the last entry applied, and the raw function last, which sits beneath the first entry applied. Mapping one to the other only needs the direction of the fold, which is why that direction is worth writing down.

saying these in an interview costs you the question

  • Says the first wrapper in the list is the outermost layer
  • Assumes the list order is the order wrappers run on the way in
  • Thinks the returning pass visits the wrappers in the same order as the entering pass
  • Treats the set of enabled wrappers as the whole specification, ignoring order
  • Appends a wrapper meant to sit next to the raw function