Twelve API design patterns on Azure, with idempotency keys implemented four times: three versions that pass a sequential retry test and fail in production, and one that holds. Companion repo for the Cloud Perspectives post "Twelve API Design Patterns, and the One Where Passing Tests Prove Nothing" (draft).
| Resource | Role |
|---|---|
API Management BasicV2, API patterns |
09 gateway, 07 rate limit per subscription, 04 problem+json for 429, 11 backend circuit breaker |
| Function App, Flex Consumption, .NET 8 isolated | the API: patterns 01-06, 08, 10-12 |
| Storage account (identity only) | tables users, orders, idempotency, ledger, reports, webhooks, settings; queues report-jobs, webhook-deliveries (+ poison) |
| Application Insights | traces tagged PAYMENT, WEBHOOK-SEND, WEBHOOK-INBOX, REPORT, BALANCE |
| # | Pattern | Code | Try |
|---|---|---|---|
| 01 | RESTful resources | Functions/Resources/UsersApi.cs |
GET/PATCH/DELETE /api/v2/users/{id} |
| 02 | API versioning | Functions/Versioning/UsersV1Api.cs |
GET /api/v1/users/{id}: Deprecation, Sunset, Link: rel="successor-version", 410 after sunset |
| 03 | Pagination (cursor) | Functions/Pagination/OrdersApi.cs |
GET /api/v2/users/42/orders?limit=20, follow next |
| 04 | Consistent errors | Core/Http/Problem.cs, Functions/Errors/ProblemMiddleware.cs, infra/apim/policy.xml |
any error; types in docs/problems.md |
| 05 | Idempotency keys | Core/Idempotency/PaymentHandlers.cs, Functions/Idempotency/PaymentsApi.cs |
POST /api/payments?impl=atomic with Idempotency-Key |
| 06 | Conditional requests | Functions/Resources/UsersApi.cs |
If-None-Match 304, If-Match 412, none 428 |
| 07 | Rate limiting | infra/apim/policy.xml |
70 calls/min through APIM: 429 + Retry-After |
| 08 | Async request-reply | Functions/AsyncReply/ReportsApi.cs |
POST /api/reports 202, poll, 303 |
| 09 | API gateway | infra/apim/apim.bicep, policy.xml |
subscription key, key injection, X-Correlation-Id |
| 10 | Webhooks | Core/Webhooks/WebhookSignature.cs, Functions/Webhooks/Webhooks.cs |
Standard Webhooks signing, queue retries, inbox dedup |
| 11 | Circuit breaker | Core/Resilience/CircuitBreaker.cs, Functions/Resilience/BalanceApi.cs; APIM backend rule |
PUT /api/dependency/mode {"mode":"failing"}, then GET .../balance |
| 12 | API aggregation | Functions/Aggregation/SummaryApi.cs |
GET /api/v2/users/42/summary: partial, unavailable |
POST /api/payments?impl= selects one of four handlers. Same store, same provider, different semantics.
| impl | What it gets wrong |
|---|---|
racy |
check-then-act: concurrent retries both charge; random provider charge id |
memory |
result kept in process memory: after a restart the key exists and the replay does not |
endpoint-scoped |
key scoped to the route: a retry through /orders/{id}/pay charges again |
atomic |
nothing: atomic claim (AddEntity), durable result (UpdateEntity + ETag), key passed to the provider, lease takeover, payload fingerprint |
Order ids starting with lost- make the simulated provider charge and then drop its first answer (the timeout case).
Prerequisites: .NET 8 SDK, Azure Developer CLI, Azure CLI, Azure Functions Core Tools. Works from Windows PowerShell 5.1.
dotnet test # Core tests, no Azure needed
azd env new api-design-patterns
azd env set AZURE_LOCATION swedencentral
azd env set APIM_PUBLISHER_EMAIL you@example.com
azd up # postdeploy: APIM named value + seed data
./scripts/race.ps1 # idempotency matrix against Azure
./scripts/race.ps1 -ViaGateway -Parallel 10 # same, through APIM (rate limit applies)
./scripts/demo.ps1 # all twelve patternsAPIM BasicV2 bills by the hour. azd down --purge when you are done.
Copy src/ApiPatterns.Functions/local.settings.sample.json to local.settings.json, start Azurite, func start in that folder, then POST http://localhost:7071/api/ops/seed.
race.ps1: the full idempotency matrix as predicted; the racy burst charged 16 times for 20 identical requests, all answered 201.demo.ps1: all twelve steps. The APIM 429 comes back as problem+json withRetry-After. 70 simultaneous calls against 60/min let 69 through: APIM rate-limit counters are approximate under a burst.- Deploy note: Function routes must not start with
admin(reserved by the host). The function still lists, every call returns an empty 404. Seed and recycle live underops/. - Not exercised yet: the APIM backend circuit breaker rule (deployed, never tripped by the demo).
- Webhook delivery defaults to
https://$WEBSITE_HOSTNAME/api/webhooks/inbox. SetWEBHOOK_TARGET_URLto deliver elsewhere.
src/ApiPatterns.Core framework-free patterns: problem details, idempotency handlers, breaker, webhook signing
src/ApiPatterns.Functions Azure Functions app (HTTP + queue triggers), Table Storage stores
tests/ApiPatterns.Core.Tests xUnit: the failure matrix, breaker states, signature checks
infra/ Bicep (subscription scope) + APIM module and policy.xml
scripts/ postdeploy, demo, race (Windows PowerShell 5.1 compatible)
images/ diagrams (SVG + PNG); images/src regenerates them from the Azure icon set
docs/problems.md the problem type catalogue
Diagrams: cd images/src, set AZURE_ICONS to the Azure icon folder, run python map.py, python failure_modes.py and python matrix.py, then python render.py ../api-patterns-azure-map.svg ../idempotency-failure-modes.svg ../idempotency-test-matrix.svg (needs Playwright).
MIT

