A policy needs a fact captured at the server edge and an operation identity that only routing produces — where should it decide?
answer
- facts travel inward, never outward
- decision floors at its latest fact
- capture outward, decide inward
- coarse guard early, precise check late
- missing captured fact must fail closed
basics
~20 sThe decision must run at or below the altitude of the latest fact it needs: capture the connection fact at the outermost hook into per-request state, then decide deeper, where the operation identity exists. Facts travel inward, never outward.
solid answer
~50 sStart from the constraint rather than from taste: a hook can only read what already exists when it runs, so the *decision* has to live at or below the altitude of the last fact it needs, and every earlier fact must be carried forward in per-request state. That usually means an outer element stamps the connection fact into the request context and a handler-aware element makes the call. Then weigh the costs. Deciding late means you have already parsed, bound and possibly started work before refusing — more expense and more attack surface — so a cheap coarse guard further out plus a precise decision inward is often the right shape. Deciding late also loses the requests refused earlier. And the split is a coupling: if the capture half is removed, the deciding half must fail closed rather than quietly permit.
go deeper
Take away the direction rule: information can be passed from an earlier hook to a later one through per-request state, never the other way round.
Be able to place the decision: it must run at or below the altitude of the last fact it needs, with earlier facts carried forward explicitly.
Show the cost reasoning — what work happens between the two altitudes, and why a cheap outer guard plus a precise inner decision often beats either alone.
Treat the split as a coupling you own: define fail-closed behaviour when the capture is missing, one derivation per fact, and a test that proves removing half refuses traffic.
## The constraint that decides the shape Everything here follows from one asymmetry. **A hook sees only what has already been computed when it runs.** Transport facts exist from the first byte; operation identity exists only after routing; bound arguments exist only after dispatch. So: - A fact observed **early** can be carried **inward**, by writing it into the per-request state the framework threads through the exchange. - A fact that only exists **later** can never be made visible **outward**. No mechanism moves it backwards in time. Therefore the decision point has a floor: it must sit **at or below the altitude of the latest fact it consumes**. Everything else in the design is a trade-off around that fixed point. ## The candidate shapes 1. **Capture outward, decide inward.** The outermost element stamps connection facts into per-request state; the handler-aware element reads them alongside the operation identity and decides. This is the default, and the only one that works when the policy genuinely needs both. 2. **Decide at the pipeline altitude, after matching.** If the route template is precise enough — the policy needs "which operation", not "which arguments" — the decision can sit in the chain once matching has happened. Cheaper than going all the way in, and it still refuses before the body is parsed and bound. 3. **Two-stage: coarse guard outward, precise decision inward.** A cheap, conservative check at the edge rejects what is obviously disallowed under any operation; the precise check runs deeper. This bounds the work an unauthorized caller can provoke without pretending the edge knows more than it does. 4. **Push the decision into each handler.** Maximum context, zero uniformity — the check is now opt-in per handler and the next handler someone writes will forget it. Reasonable only when the policy is genuinely per-operation logic rather than a cross-cutting rule. ## How to choose between them | Consideration | Pulls the decision outward | Pulls the decision inward | |---|---|---| | Cost of work done before refusing | Refuse before parsing, binding, and any handler work | Accepts that cost in exchange for precision | | Precision available | Only transport and raw text facts | Operation identity and converted arguments | | Coverage | Sees refused and unmatched traffic too | Sees only requests that reached a handler | | Uniformity | One place, applies by construction | Applies only where the attachment point applies | | Portability across frameworks | Depends least on framework internals | Depends most on the handler model | | Blast radius of a mistake | Wrong refusal affects everything | Wrong refusal is scoped to the operation | ## The failure modes to design against - **Fail-open on a broken split.** If the capture half is removed, renamed, or simply not registered on some path, the deciding half reads an absent value. It must treat "fact missing" as a refusal, not as a permissive default. Write the test that removes the capture and asserts a refusal. - **Two sources of truth for the same fact.** Once a transport fact is copied into per-request state, something deeper may also try to derive it — for example from a forwarded header. Pick one derivation, do it once, at the altitude that can validate it. - **A decision that outruns its coverage.** A policy that must hold for *every* request cannot live only at an altitude another stage decides the coverage of. - **Silent divergence between the coarse and precise checks.** In the two-stage shape, the outer guard and the inner decision encode overlapping rules; when one is updated and the other is not, the system's behaviour is whichever is stricter, which is rarely what anyone reasoned about. ## What to say out loud in an interview Name the constraint first, then the shape, then the cost you accepted. A strong answer sounds like: "the decision has to run where the operation is known, so the transport facts get captured outward and read inward; I keep a cheap conservative guard at the edge so an unauthorized caller cannot make us parse a large body; and the deciding half fails closed if the captured fact is absent, with a test that proves it." A weak answer picks an altitude by habit and never mentions that one of the two facts cannot be seen there. ## Frameworks differ here Frameworks vary in how much of this is handed to you: some carry a rich per-request context that any altitude can read and write, others give you a plain attribute map, and others expect the value to be threaded explicitly through the call. The capability changes the ergonomics of the split, not the constraint behind it — the order in which facts come into existence is a property of request processing, not of any one framework.
- Why is a cheap conservative guard at the outer altitude often worth keeping even when the real decision runs deeper?Because everything between the two altitudes is work an unauthorized caller can make you do — reading and buffering a body, converting arguments, touching dependencies. A coarse guard that refuses only what is disallowed under every operation bounds that work without claiming knowledge the edge does not have. Its rules must stay strictly weaker than the precise check, or the two will disagree.
- How do you keep a split policy from failing open when the capture half stops running?Make absence a refusal. The deciding half treats a missing captured fact as a denial rather than a default, and a test removes or disables the capture and asserts that requests are refused. Registering both halves through one composition unit also helps, so a path that gets the decision cannot silently miss the capture.
- When is pushing the decision into individual handlers defensible rather than a smell?When the rule is genuinely about one operation's semantics rather than a cross-cutting property — the condition depends on the resource's own state or on relationships only that operation understands. It stops being defensible the moment the same rule is copied into a second handler, because uniformity by construction is exactly what the hook altitudes provide.
- What tells you the decision could move up to the pipeline altitude instead of the handler-aware one?That the policy only needs the operation's identity, not its converted arguments. If the route template and the handler's declared metadata answer the question, deciding right after matching refuses earlier, avoids parsing and binding, and stays closer to a framework-neutral mechanism. Needing an argument value forces the decision inward.
saying these in an interview costs you the question
- Claims the outermost hook can learn which operation will run
- Carries a fact backwards from a deeper hook to an earlier one
- Lets a missing captured fact default to allowing the request
- Picks an altitude by habit without naming the facts it needs
- Duplicates the full rule at two altitudes and lets them drift
- Ignores that deciding late means work already done for a refused request