Everything the gateway does is a policy
After Part C the gateway can model an API and forward it, and that is all — every request currently sails straight through untouched. All the interesting behaviour, the entire reason you paid for a front desk, is expressed as policies: small XML documents that the gateway executes around each request. Authenticate the caller, count the call, reject the eager, rewrite a header, cache the answer, hide an error — each is a policy statement, and together they are the program the gateway runs on every request. If Part A taught you the language of a request, policies are the sentences you write in it.
- Policy
- A statement in APIM's XML policy language that runs as part of the request pipeline. Policies attach at four scopes — global, product, API, or operation — and the ones at each scope combine, so a global rule and an operation-specific one both apply. This is how one rate limit can cover everything while one operation gets an extra rule of its own.
The scopes matter more than they first appear. A limit set at the product scope governs every consumer of that product; a transform set at the operation scope touches only that one call. You compose behaviour by choosing the right altitude — broad rules high, specific rules low — which is the same layering instinct RBAC taught you in Class Eight, applied to traffic instead of permissions.
The pipeline — four stages, one red rail
Policies do not run in a heap; they run in a fixed pipeline with four stages, and knowing the order is how you reason about what runs when. Inbound policies run before the backend is touched — authenticate, rate-limit, validate, transform the request, or reject it outright, so a bad call never costs a backend cycle. Backend policies shape the forwarding itself — retries, timeouts, which backend. Outbound policies edit the response on its way back — strip internal headers, cache the result, reshape the body. And on-error catches whatever failed at any stage and decides what the caller learns — which should be little, because a backend stack trace is a reconnaissance gift.
The policies you will actually write
The reference lists dozens; the working set is small. Rate-limit-by-key and quota-by-key are the per-consumer arithmetic — short-window bursts and long-window totals, counted per subscription. Validate-jwt checks the caller's Entra token at the door, so unauthenticated requests never reach the backend (38f). Cache-lookup and cache-store make repeated answers stop costing backend calls. The transformation family rewrites URLs, sets or strips headers, and reshapes bodies. And CORS is the one browsers force on you: a policy that tells a browser which web origins may call this API from a page, answering the pre-flight OPTIONS request so a legitimate single-page app is not blocked while a hostile site still is.
Here is a real inbound policy — readable, four behaviours, no magic:
<!-- inbound: applies before the backend is touched -->
<inbound>
<cors>
<allowed-origins><origin>https://shop.campux.co</origin></allowed-origins>
<allowed-methods><method>GET</method></allowed-methods>
</cors>
<validate-jwt header-name="Authorization" failed-validation-httpcode="401">
<openid-config url="https://login.microsoftonline.com/<tenant>/v2.0/.well-known/openid-configuration" />
</validate-jwt>
<rate-limit-by-key calls="100" renewal-period="60"
counter-key="@(context.Subscription.Id)" />
</inbound>
Read it top to bottom the way the gateway does: tell the browser who may call, reject anyone without a valid token as a 401, then allow each subscription 100 calls a minute and answer the 101st with a 429. Four lines of consequence, all traffic-shaped, none of it business logic. That last property is not an accident — it is the discipline the next section is about.
Two disciplines that keep policy from becoming a liability
First, policies are code. A rate limit is production behaviour; a JWT check is a security control. So the XML lives in the repository, is deployed by the Class Twenty-Two pipeline, and is reviewed like everything else — because portal-edited policy is the Class Twenty click-drift problem reborn, a production change with no diff, no review, and no way to reproduce. The moment someone "just tweaks a policy in the portal," you have an environment whose behaviour no file describes.
Second, policies stay traffic-shaped. Authentication, limiting, caching, transformation, header hygiene — the gateway's proper work is about how requests flow. The moment business logic creeps into policy XML — a discount calculation, a pricing rule, an entitlement decision — you have hidden part of the application in a place no debugger visits, outside the test suite, outside the code review path most teams wire for application code. Six months later a pricing bug hunt ends somewhere nobody thought to look. The boundary is the track's spine, stated once more: the gateway decides whether and how a request travels; the backend decides what it means.
Business logic in policy is a bug hunt in a place no debugger visits.
The feral sync, caught as a 429
On the catalog API's inbound policy, Campux sets one rule: rate-limit-by-key at 100 calls a minute, counted on the subscription ID, with a quota-by-key ceiling per day behind it. It is committed to the repo and shipped by the pipeline like any code. For weeks nothing seems to happen — which is what a good limit looks like.
Then, in week six, one marketplace's nightly sync goes feral: a bug on their side retries in a tight loop and starts hammering the catalog. Under the old shared-backend plan this is a Class Thirty-Six incident — the backend saturates, every partner slows, and the on-call engineer spends the small hours guessing whose traffic it is. Instead it is a non-event: the offending key throttles at its own limit, the gateway answers the excess with a clean 429 and a Retry-After, the other two partners never feel a thing, and the §2 usage data names the culprit before their own team has found the bug. Monday's email goes to one inbox. Four lines of policy converted an outage into a status code — which is the entire promise of the front desk, paid off once.
Behaviour, but not yet identity
The front desk now has teeth: it authenticates, limits, caches, transforms, and refuses politely. But look back at that rate-limit line — it counts on context.Subscription.Id, and we have not yet said where a subscription comes from or how a caller gets one. The policies assume a known consumer; Part E is where a consumer becomes known. Then Part F hardens the door's locks, and Part G decides where on the network it stands. Policy is the behaviour; the next parts are the identity and the walls that behaviour needs to mean anything.
The four lines that end the 3am pages
One consumer's runaway job has twice saturated a shared backend and twice woken the on-call. You add a per-key rate limit and a daily quota, commit them, and ship them through the pipeline. The next time that job misbehaves, it throttles itself into a 429, everyone else is untouched, and the usage chart names it by morning. You did not out-argue the noisy team; you made their bad night self-correcting and legible. Four lines of reviewed XML retired a recurring incident — and that is the unglamorous work that keeps a pager quiet.
Examination
Four drills, then two situations. The situations have no marking scheme — write your answer before you reveal the reasoning, or the exercise is worthless. Nothing is stored; this is between you and the page.
A — inbound, because the whole point is to stop a bad request before it costs anything. Inbound runs first; a failed token check there returns a 401 and the backend never hears the request, which is both cheaper and safer. B misunderstands the front desk: the backend may also check (defence in depth, 38f), but relying on it alone means unauthenticated traffic reaches it, which is exactly what the gateway exists to prevent. C is too late — outbound runs after the backend already answered, so you would have served the request before deciding it was unauthorised. D confuses a control with error handling: on-error decides what a caller learns when something breaks, not whether they were allowed in. The pipeline order is not trivia; it is what lets a gateway reject cheaply.
B — CORS is a browser rule, not a server one. A browser will not let a page at one origin read a response from another unless the API says that origin is allowed, and it checks by sending a pre-flight OPTIONS first. curl obeys no such rule, which is why the exact same request succeeds from the terminal and fails from the page — the classic symptom that tells you it is CORS and not auth or availability. The fix is a cors policy naming https://shop.campux.co as an allowed origin and the methods it may use. A and C invent failures the symptom rules out (curl works; the key is fine). D reaches for the wrong layer entirely — CORS is not a port or firewall matter, and "just open it" here means allowing every origin, which hands your API to any hostile page.
Limit, authenticate, cache-and-strip — traffic work, every one. The test all three pass: they concern how requests flow, not what the application means, and they apply uniformly across the operations behind the gateway. The rejects fail it from opposite directions. Discount rules in policy expressions is business logic hidden in XML — outside the debugger, outside the test suite, outside the repo review path most teams wire for application code — and six months later a pricing bug hunt ends in a place no developer thought to look. And returning raw exception detail inverts on-error's whole purpose: stack traces, connection-string shapes, and library versions are a reconnaissance packet, and "easing partner debugging" is what correlation IDs and the developer portal are for. The boundary sentence: the gateway decides whether and how a request travels; the backend decides what it means.
# catalog API — policy plan
1. rate-limit-by-key on the subscription, 100/min,
with a daily quota-by-key behind it.
2. validate-jwt on inbound; reject bad tokens 401.
3. Compute the customer's loyalty discount in a
policy expression, so pricing is fast at the edge.
4. On outbound, strip internal headers and cache
GETs for 30 seconds.
Line three — and notice it is defended with a real-sounding benefit ("fast at the edge"), which is how this mistake actually ships. A loyalty discount is what a price means, not how a request travels — it is application logic, and putting it in policy XML exiles it from the debugger, the test suite, and the code-review path the rest of the pricing code lives in. When the discount is wrong six months later, the bug hunt combs the backend for a rule that turns out to live in a gateway policy nobody thought to open. "Close to the edge" is a genuine performance idea that belongs to caching, not to relocating business rules. The distractors are the policy working: per-key limits and quotas are textbook gateway work (A is wrong to move them), inbound is exactly where JWT validation belongs so bad tokens cost nothing (B), and stripping headers plus caching GETs is precisely outbound's job (D). The reviewer's reflex: when logic is justified by speed, check whether it is really meaning being smuggled out of the backend.
In the moment, let the fix stand. During a live incident, unblocking the partner is the right call, and insisting on a pull request while revenue burns is exactly the "root cause before mitigation" mistake Class 36 warns against. So the immediate answer is yes — make it live. The discipline is not about the emergency; it is about what happens at 9am.
After, name what the portal edit actually created: an environment no file describes. The gateway now behaves in a way the repo does not capture, which means the next deploy from the pipeline will silently revert the fix — the partner breaks again, and nobody connects it to a deploy because the change that vanished was never in a diff. Portal drift is not a tidiness preference; it is a latent outage with a delay timer. "Later" is where that timer hides.
So make "later" a step in the incident, not a good intention. The postmortem's action item is concrete: reproduce the portal change as XML in the repo, open the PR, and deploy it through the pipeline so the live state and the file agree again — before the incident is closed. Wire a check, if you can, that flags portal edits so drift is visible rather than discovered. The sentence to leave them with: the portal fix saved the partner tonight; putting it in the repo tomorrow is what stops the next deploy from un-saving them.
Walk the stages in order, because the order is the point. "A request hits inbound first — that's where I authenticate it, rate-limit it, and shape it, so a bad or unauthorised call is rejected before the backend spends a cycle. If it passes, backend policies handle the forwarding — retries, timeouts. The response comes back through outbound, where I strip internal headers and cache. And underneath all three is on-error: anything that fails, at any stage, lands there." Naming inbound-first shows you understand why a gateway is cheap to say no with.
Then point at the red rail, because that is the senior detail. "The stage most people skip is on-error, and it is the one an auditor reads first. What it returns is the difference between a clean 429 with a Retry-After and a raw stack trace that leaks the backend's shape, library versions, and maybe a connection string. I keep on-error terse for the caller and detailed only in the logs — a correlation ID out, the forensics kept in the Class 28 workspace."
Close on where teams get it wrong: the boundary. "The recurring mistake isn't in the pipeline mechanics — it's people smuggling business logic into policy XML because it feels fast at the edge. Then a pricing bug lives somewhere no debugger looks. My rule is that policy stays traffic-shaped — auth, limits, caching, transforms — and anything about what the request means stays in the backend. Also: policies are code, so they live in the repo and ship through the pipeline, not the portal." Those two disciplines, said out loud, are what separate someone who has run an APIM in production from someone who has clicked through the portal once.
Five things worth carrying out of this part
- Everything the gateway does is a policy — XML attached at global, product, API, or operation scope, combined by altitude. Broad rules high, specific rules low.
- Policies run in a fixed pipeline: inbound (before the backend), backend (the forwarding), outbound (the response), and on-error (any failure). Reject in inbound so a bad call costs nothing.
- The working set is small: rate-limit-by-key and quota-by-key, validate-jwt, cache-lookup/store, the transform family, and CORS — the browser rule that names which web origins may call the API.
- Policies are code: repo, pipeline, review. Portal-edited policy is drift — a production change with no diff that the next deploy silently reverts.
- Policies stay traffic-shaped. Business logic in policy XML is a bug hunt in a place no debugger visits. The gateway decides whether and how; the backend decides what it means.
- Policy availability differs by tier: the by-key limiting policies and several others need Basic v2 or above (or the classic Developer/Basic tiers), and the serverless Consumption tier supports a reduced set. This is the same tier-gating from 38b, seen from the policy side — check the policy reference's tier table before you design around a specific policy, because "it's in the docs" does not mean "it's in your SKU." ↩
- Rate-limit-by-key and quota-by-key are a pair worth keeping straight: rate-limit is a short-window burst control (calls per 60 seconds), quota is a long-window total (calls per day or month). You usually want both — the rate limit stops a caller flooding you in a minute; the quota stops them consuming a month's allowance in an afternoon of polite, under-the-rate-limit calls. ↩