skip to content

As a service grows to many endpoint families, how would you structure middleware scopes so the applied policy stays auditable?

level: principalimportance: nice to knowfreq 42%

answer

  1. audience is the axis, not feature
  2. one branch, one chain, declared once
  3. predicate only for runtime conditions
  4. policy should be readable at registration
  5. generate an inventory of route to chain

basics

~20 s

Branch the route tree by caller audience rather than by feature, give each branch one explicit chain, and reserve predicates inside hooks for genuinely runtime conditions. The hooks running for an endpoint should be readable from its registration.

solid answer

~50 s

Decide first what the audiences are — unauthenticated public, end-user, machine-to-machine, internal or administrative, inbound webhooks — because those, not features, are what differ in policy. Give each audience a branch of the route tree with one chain declared in one place, and register feature routes into the branch that matches their audience. Keep the genuinely universal hooks global and small. Use a predicate inside a hook only when the condition cannot be known at registration — a tenant setting, a negotiated content type, a rollout flag — because a predicate hides scope from everyone reading the routes. Then make it checkable: a test or a startup report that lists every route with the chain that will run for it, so a reviewer sees a change in applied policy as a diff rather than as an argument.

go deeper

for a junior

Focus on the smaller version of this: know which endpoints are public and which are not, and keep that difference visible where routes are declared rather than buried in a condition.

for a middle

Explain the two mechanisms and when each fits. Branch at registration when the condition is known then; use a predicate only for something that cannot be known until the request arrives.

for a senior

Show the control that survives turnover: a generated inventory of route to chain, assertions on the credential-free set, and exceptions that carry an owner and a reason.

for a principal

Own the tradeoffs explicitly — shared core against per-branch drift, platform defaults against team autonomy, and which ordering constraints are reserved so no team can register outside them.

In a small service, scoping is a local decision: attach this here, that there. Past a few dozen endpoints and more than one team, scoping becomes a policy surface, and the question stops being *what should this hook do* and becomes *how does anyone know what runs for a given endpoint*. ## Branch by audience, not by feature The instinct is to mirror the domain: an orders branch, a customers branch, a reporting branch. That grouping is useful for code organisation and almost useless for policy, because two order endpoints may face completely different callers. The grouping that carries policy is the **audience**: - **Public** — reachable with no credentials at all: probes, discovery documents, the token endpoint. - **End-user** — authenticated as a person, usually session-oriented, with the cross-site and cross-origin concerns that follow. - **Machine-to-machine** — authenticated as a client, different limits, different error verbosity. - **Internal or administrative** — additional network or identity constraints, heavier audit. - **Webhooks** — authenticated by signature over the raw body, which usually forces a different body-handling order from everything else. Each audience gets one branch with one chain declared once. Feature routes are then registered into the branch their audience implies, and the policy for an endpoint is a fact about where it lives. ## Branch by path versus branch by predicate There are two mechanisms for making a hook apply to some requests and not others, and they differ mainly in where the decision is *visible*. | | Branching at registration | Predicate inside a hook | |---|---|---| | Where the decision is written | The route tree | The hook's body | | Evident to a reader of the route | Yes | No | | Can depend on runtime state | Only via what the hook does | Yes, that is its purpose | | Failure when nobody maintains it | A route filed in the wrong branch | A condition that silently stops matching | | Cost | Paid only inside the branch | Paid on every request | The rule that follows: branch when the condition is known at registration time, predicate only when it genuinely is not. A predicate testing a path is almost always a branch written in the wrong place, and it carries the extra hazard that it compares a raw target rather than the route the router chose. ## Make the applied policy inspectable Structure alone decays; what keeps it honest is being able to answer, mechanically, *which hooks run for this endpoint?* Practical forms: 1. **A generated inventory.** Walk the registered routes at startup or in a test and emit each route with its resolved chain. Check it in, and let a change to any hook's reach show up as a diff in review. 2. **Assertions on the properties that matter.** The set of routes reachable without credentials; the set that skips the request-size limit; the set where the body is read before a signature is verified. Each is a short, high-value test. 3. **A budget on exceptions.** Exceptions are fine; unbounded exception lists are not. Requiring an owner and a reason for each keeps the list enumerable. ## The tradeoffs a lead actually owns - **Duplication versus drift.** One chain shared by all branches is easy to reason about but forces every audience into one policy; per-branch chains fit reality but drift apart. The usual compromise is a small shared core plus explicit per-branch additions. - **Fewer, wider branches versus many, narrow ones.** Wide branches are easy to audit but push exceptions into predicates; narrow ones are precise but multiply the places a policy must be repeated. - **Platform defaults versus team autonomy.** Whether hooks are provided by a platform layer that services cannot remove, or assembled per service, decides who is accountable when an endpoint is exposed. A platform default that teams can silently opt out of is the worst of both. - **Ordering as a contract.** Once several teams register hooks, the relative order of a few of them is load-bearing. Naming a small number of reserved stages, and refusing to let anything be registered outside them, is cheaper than discovering the dependency during an incident. The measure of a good answer here is not a particular layout. It is whether a new engineer can look at one endpoint's registration and correctly predict what will run for it, and whether a change to that prediction is visible to a reviewer.

  • Why do inbound webhooks so often need their own branch?
    Because they are authenticated by a signature computed over the exact bytes received, which constrains body handling: nothing may consume, re-encode or normalise the body before verification. That ordering requirement conflicts with the parsing chain the rest of the service wants, so it is cleaner as a separate branch than as an exception inside a shared one.
  • What is the strongest argument against a predicate inside a global hook?
    It makes scope invisible. Nothing in the route declaration reveals that the hook applies here and not there, so the only way to answer what runs for an endpoint is to read every hook's body, and a condition that quietly stops matching produces no error anywhere.
  • How do you stop per-branch chains from drifting apart?
    Factor the genuinely universal part into one shared definition that every branch composes, and keep per-branch additions short and explicit. Then assert the invariants that must hold everywhere — the size limit, the correlation identifier — rather than trusting each branch to have remembered them.
  • What does a route-to-chain inventory buy that code review does not?
    It turns an invisible change into a visible one. Adding a hook, moving a route between branches or widening a condition all alter the inventory, so the reviewer sees the effect on applied policy instead of inferring it from a registration line they may not recognise as significant.

saying these in an interview costs you the question

  • Groups routes by feature and assumes policy follows the domain
  • Solves every exception with another condition inside a global hook
  • Cannot say which hooks run for an endpoint without reading hook bodies
  • Lets each team assemble its own chain with no shared invariants
  • Treats a growing exception list as normal rather than as debt
  • Relies on reviewers noticing a missing hook that appears nowhere in the diff