When you stack a permission-check decorator and a caching decorator on one Python handler, which one must be on top?
answer
- Ask which layer can return early
- A short-circuit hides everything beneath it
- A hit never reaches the inner wrapper
- Correct on the first call only
- Checks above caches
basics
~20 sThe permission check goes on top, outermost. A caching decorator returns early on a hit, so anything beneath it is skipped for that call — with the cache outermost, a cached result is served without the check ever running.
solid answer
~50 sPut the check above the cache. Stacked decorators execute top-down, and a caching wrapper is a **short-circuiting** layer: on a hit it returns a stored value without ever calling the layer beneath it. If the cache is outermost, the first successful call populates it and every later caller with the same arguments gets that value without the permission wrapper being entered — an authorization bypass that no test of the first call will catch. With the check outermost, it runs on every call and only authorized calls reach the cache. The general rule this instance illustrates: **anything that must observe or reject every call belongs above anything that may return early.** The same reasoning places rate limiting and request validation above caches, and puts per-user identity into the cache key when the cached value is not identical for everyone.
code
python · 27 linesCACHE = {}
def cached(f):
def wrapper(user, key):
if key not in CACHE:
CACHE[key] = f(user, key)
return CACHE[key]
return wrapper
def requires_admin(f):
def wrapper(user, key):
if user != "admin":
raise PermissionError(user)
return f(user, key)
return wrapper
@cached # outermost: a cache hit never reaches the check
@requires_admin
def report(user, key):
return f"report:{key}"
print(report("admin", "q3"))
print(report("guest", "q3"))go deeper
Recall the direction: the topmost decorator runs first at call time, so the check on top is the one that always runs. Be able to point at which line is outermost.
Explain the mechanism — a caching wrapper returns before delegating, so every layer below it is skipped on a hit — and show the bypass with a two-call example.
Frame it as a class of bug: wrong ordering is correct on a cold cache and wrong after warm-up, so it escapes single-call tests. Also raise the cache-key identity question separately.
Set the house rule so nobody re-derives it per pull request: observe-every-call layers outermost, short-circuiting layers inside them, and a review checklist item for shared caches keyed on identity.
### The rule in one line Stacked decorators execute top-down, so a layer that can **return without delegating** hides everything beneath it. A check that must run on every call therefore has to sit above any such layer. A cache is the canonical short-circuiting layer, and a permission check is the canonical must-always-run layer, which is why this specific pairing is the standard interview vehicle for the ordering question. ### What goes wrong concretely Take a handler that returns a report and is decorated with a memoising wrapper and a wrapper that raises unless the caller is an administrator. Written with the cache on top: ```python @cached @requires_admin def report(user, key): ... ``` the first call by an administrator runs the check, runs the body, and stores the result under the cache key. Every subsequent call with the same key — by anyone — hits the cache and returns the stored value immediately. The `requires_admin` wrapper is never entered. Nothing raises, nothing logs, and the behaviour is *correct on a cold cache*, which is exactly why it survives review and unit tests that exercise a single call. It is a real authorization bypass with a warm-up window. Flip the stack: ```python @requires_admin @cached def report(user, key): ... ``` Now the check is outermost. It runs on every single call, unauthorized callers are rejected before the cache is consulted, and the cache only ever serves callers who have already passed. This is the correct default. ### Generalising past this one pair The pairing is a special case of a rule worth stating explicitly, because it settles most stacking arguments: 1. **Layers that must observe every call go outermost.** Authentication and authorization, rate limiting, request-count metrics, audit logging of attempts. 2. **Layers that may short-circuit go inside those.** Caches, memoisation, feature-flag gates, circuit breakers — anything whose whole purpose is to skip work. 3. **Layers that transform inputs or outputs go where the transformation is meant to apply.** A wrapper that normalises arguments must sit above the cache if the cache is to key on normalised arguments; a wrapper that serialises the result sits below the cache if you want the serialised form cached, and above it if you want the raw object cached. 4. **Layers that must see the real body** — timing the actual work, retrying the actual work — go innermost, because anything above them measures or retries the layers in between as well. Applying rule 3 shows the ordering question is not only about security. If argument normalisation sits *below* the cache, two calls that differ only cosmetically produce two different cache keys and the hit rate collapses; move it above the cache and they collapse into one key. Neither placement raises an error; only one is what you meant. ### The cache-key half of the problem Ordering fixes *whether* the check runs; it does not by itself make a shared cache safe. If the cached value differs per caller, the caller's identity must be part of the cache key as well — otherwise the check correctly rejects unauthorized callers while authorized caller A is served the value computed for authorized caller B. In review, treat "is the check above the cache?" and "does the key include everything the value depends on?" as two separate questions, both of which must be answered yes. ### Reasoning about it in an interview The way to derive the answer rather than recall it: rewrite the stack as nested calls and ask what happens on the *second* call. `cached(requires_admin(report))` — the second call enters `cached`'s wrapper, finds the key, returns. Nothing else executes. `requires_admin(cached(report))` — the second call enters `requires_admin`'s wrapper, which raises or delegates, and only then reaches `cached`. Once you can say "which layer can return early, and what does it hide?" the ordering is mechanical, and the same sentence answers the rate-limit, feature-flag and circuit-breaker versions of the question without any new memorisation. ### The failure mode to name The reason interviewers like this question is that the wrong order fails *asymmetrically*: it is correct on the first call and wrong afterwards, so it passes the obvious test, produces no exception, and only misbehaves once the process has been running long enough to have a populated cache. Being able to say that out loud — "this is a bug that only appears after warm-up" — is what separates a candidate who has reasoned about stacking from one who has memorised the direction.
- Where would you place a rate-limiting decorator relative to a caching one, and why?Above the cache, for the same reason as the check: the limiter exists to count and reject *calls*, and a cache hit that returns beneath it is a call the limiter never sees. Placed under the cache it would only ever count misses, which silently raises the effective limit for any caller repeating the same arguments.
- If a wrapper normalises arguments before the body sees them, does it belong above or below the cache?Above, if you want the cache keyed on normalised arguments — otherwise two calls that differ only cosmetically produce two keys and the hit rate suffers. Below, only if the raw arguments are genuinely the identity of the result. Neither placement errors, so the choice has to be deliberate and stated in a comment.
saying these in an interview costs you the question
- Says the order does not matter, both just wrap
- Puts the cache outermost to save the check's cost
- Thinks a cache hit still runs the inner wrappers
- Tests only the first call and declares it correct
- Assumes ordering alone makes a shared cache safe
- Confuses application order with execution order here