What is REST? What is the difference between GET, POST, PUT, PATCH and DELETE, which of them are idempotent, and which status codes should an API return?
19 reports in the question index (9 linked below), from 2025 to this month: Infosys (4), Deloitte, TCS, UST, Capgemini and others. Asked as a basic in the first technical round, then pushed into idempotency and retries.
Say this first · about 30 seconds
REST is a style for designing web APIs around resources. Each thing, like a contract or a payment, has its own address, and you act on it with the standard HTTP methods instead of inventing verbs in the URL. The server keeps no session between calls, so every request carries everything it needs, which is what lets us run several copies of the API behind a load balancer.
GET reads and never changes anything. POST creates, or starts something, under a collection. PUT replaces the whole resource with what you send, and PATCH changes only the fields you send. DELETE removes it.
Idempotent means sending the same request twice leaves the server in the same state as sending it once. GET, PUT and DELETE are idempotent, POST is not, and PATCH isn't guaranteed to be. That matters because clients and retry policies resend requests after a timeout.
For status codes I keep to a small set, grouped by who is at fault. 2xx means it worked: 200 for a normal read, 201 Created with a Location header after a POST, and 204 when there's no body. 4xx means the request was wrong: 400 for bad input, 401 when you're not signed in, 403 when you're signed in but not allowed, 404 when it doesn't exist, and 409 for a conflict. 5xx means we failed: 500 for our own bugs. Errors come back as ProblemDetails, so every client parses them the same way.
The production lesson for us at BMW FS was retries. A client retried a payment POST after a timeout, and some customers were charged twice. Now any POST that moves money requires an idempotency key, and the API stores the key with its result, so a repeat gets the first answer back instead of a second payment.
Make it yours · Navistar Financial credit applications (invented example)
Situation: Navistar Financial truck dealers send from their own dealer systems to our Credit Application API. When a dealer system submitted an application twice, the API threw an exception and answered 500, so the dealer's system retried, again and again, and the got paged for an error that wasn't a bug.
What I did: the submit action (a controller method that handles one kind of request, see ) now answers with the status code that says what really happened: 404 if the application doesn't exist, 409 Conflict if it was already submitted, and 202 Accepted when it starts the credit decision, which finishes later.
Result: dealer systems stop retrying on 409. HTTP itself doesn't stop them: their retry policy treats a 4xx code as "the request is wrong, don't repeat it". Pages for this error went to zero, and real 500s stand out again.
What I'd say: a 500 means our bug and invites a retry. A request that conflicts with the resource's state is a 409, so the client knows not to send it again.
The code, simplified
Invented to show the shape, in C#. Tap Next to walk a repeat submit of application 7310, then a new application, 7311.
Navistar.sln · where this file sits
Controllers/
Diagram · step 1A dealer system POSTs to submit application 7310 again. It sent it yesterday.
Dealer systema Navistar dealer
Credit app APISubmit action
Credit decisionruns later
POST /7310/submit
ApplicationsController.cs · line 4 · 1 of 9The service that finds and updates applications. DI passes it in the constructor (not shown).
Fix later in your repoExample written for this card: invented, not from a real codebase.
Say it out loud, then check
Show the 30-second answer
REST is a style for designing web APIs around resources. Each thing, like a contract or a payment, has its own address, and you act on it with the standard HTTP methods instead of inventing verbs in the URL. The server keeps no session between calls, so every request carries everything it needs, which is what lets us run several copies of the API behind a load balancer.
GET reads and never changes anything. POST creates, or starts something, under a collection. PUT replaces the whole resource with what you send, and PATCH changes only the fields you send. DELETE removes it.
Idempotent means sending the same request twice leaves the server in the same state as sending it once. GET, PUT and DELETE are idempotent, POST is not, and PATCH isn't guaranteed to be. That matters because clients and retry policies resend requests after a timeout.
For status codes I keep to a small set, grouped by who is at fault. 2xx means it worked: 200 for a normal read, 201 Created with a Location header after a POST, and 204 when there's no body. 4xx means the request was wrong: 400 for bad input, 401 when you're not signed in, 403 when you're signed in but not allowed, 404 when it doesn't exist, and 409 for a conflict. 5xx means we failed: 500 for our own bugs. Errors come back as ProblemDetails, so every client parses them the same way.
The production lesson for us at BMW FS was retries. A client retried a payment POST after a timeout, and some customers were charged twice. Now any POST that moves money requires an idempotency key, and the API stores the key with its result, so a repeat gets the first answer back instead of a second payment.
Check your answer: tick what you said
You said 0 of 9
Then add your example: Navistar Financial credit applications (invented example)
Situation: Navistar Financial truck dealers send from their own dealer systems to our Credit Application API. When a dealer system submitted an application twice, the API threw an exception and answered 500, so the dealer's system retried, again and again, and the got paged for an error that wasn't a bug.
What I did: the submit action (a controller method that handles one kind of request, see ) now answers with the status code that says what really happened: 404 if the application doesn't exist, 409 Conflict if it was already submitted, and 202 Accepted when it starts the credit decision, which finishes later.
Result: dealer systems stop retrying on 409. HTTP itself doesn't stop them: their retry policy treats a 4xx code as "the request is wrong, don't repeat it". Pages for this error went to zero, and real 500s stand out again.
What I'd say: a 500 means our bug and invites a retry. A request that conflicts with the resource's state is a 409, so the client knows not to send it again.
See it
Clientdealer portal
Contracts APIASP.NET Core
DatabaseContracts table
GET /api/contracts/42
read
200 OK + JSON
1/8GET reads contract 42. Nothing changes. The answer: 200 OK and the JSON.A GET for contract 99, which doesn't exist: 404 Not Found with a ProblemDetails body.POST to the collection creates contract 43: 201 Created, its address in .PUT replaces all of contract 43 with the body sent.PATCH sends only the new phone number. Every other field stays as it was.DELETE removes contract 43: 204 No Content. Again: 404, but it is still gone.A POST with a negative amount gets 400 Bad Request and a ProblemDetails body naming the field.Address = the resource. Method = what to do. Status code = what happened.
In plain words
An API is a program's front door for other programs: the dealer portal, the mobile app or a dealer's own system send it requests over HTTP and get data back, usually as JSON.
REST is a set of rules for designing that door. Everything the API offers is a resource with its own address: /api/contracts/42 is contract 42, /api/contracts/42/payments its payments. Instead of verbs in the address (/getContract), you use the HTTP method to say what to do.
Stateless: the API remembers nothing about you between requests (). Every request carries what it needs, like the sign-in token, so any copy of the API can answer it. That is why you can run several copies behind a load balancer, a front door that spreads requests over them.
GET: read a resource. It must never change anything, which is called safe.
POST: create something new in a collection (the list address, like /api/contracts;POST /api/contracts/42/payments makes a payment), or start a process. Each POST can create another one.
PUT: replace the whole resource with what you send. PATCH: change only the fields you send, like just the phone number. DELETE: remove it.
Idempotent: doing it twice leaves things the same as doing it once. Deleting contract 44 twice: it is gone either way. Posting a payment twice: two payments. GET, PUT and DELETE are idempotent; POST is not; PATCH may or may not be (it can mean "add 1", which gives a different result each time).
Retry and idempotency key: a retry is the client sending the same request again after a timeout. An idempotency key is a unique id the client makes once per payment and sends with every try. The API saves the key with the result, and answers a repeat with the saved result instead of making a second payment.
Status code: the three-digit number at the start of every answer. 2xx means it worked (200 OK; 201 Created, which also sends a header: a line of extra information with the answer, here the address of the new thing; 202 Accepted: received, finished later; 204 No Content). 4xx means the request was wrong (400 bad input, 401 not signed in, 403 not allowed, 404 not found, 409 conflict). 5xx means the server failed (500).
ProblemDetails: the standard JSON shape for an error: type,title,status and details. ASP.NET Core sends it for you.
The senior answer · about 2 minutes
REST, in my words
REST means the API is built around resources, not actions. A contract lives at /api/contracts/42; its payments at /api/contracts/42/payments. You read with GET, create with POST, replace with PUT, change part with PATCH and remove with DELETE, and the status code tells the client what happened.
The constraint I care most about in production is statelessness: the API keeps no session between calls, each request carries its token. That is why we can run three copies of the API on and any of them can answer any request ().
Verbs and idempotency
PUT sends the whole resource and replaces it, so sending it twice gives the same result: idempotent. PATCH sends only the changed fields; ours are idempotent because they set values, but the standard doesn't promise it, since a PATCH could say "add 1". POST is the one that is never idempotent: two POSTs make two payments.
Idempotency matters because requests get repeated without anyone deciding to: a timeout, a retry policy, a user double-tapping. In .NET the standard resilience handler on retries every method by default, POST included. We call on clients that send money, and the API requires an idempotency key anyway.
Status codes I actually use
201 with a header after a create, 204 after a delete, 202 when work continues in the background. For errors: 400 for invalid input, 401 versus 403 for who you are versus what you may do, 404, and 409 when the request clashes with the resource's state, like submitting an application twice.
The rule underneath: 4xx means the client must change something and shouldn't simply retry; 5xx means we failed and a retry may help. Every error body is ProblemDetails, so clients parse one shape, and the status code is what and our alerts count. A 200 with an error inside hides failures from all of them; that is the first bug below.
In production
Money-moving POSTs require an Idempotency-Key header. The API stores the key with the response in a table with a unique index (the database refuses a second row with the same key), and a repeat returns the stored response. That came out of the incident in Deep, where retried payment POSTs charged customers twice.
No personal data in URLs: a GET's query string is encrypted on the wire, but it lands in logs and browser history. Customer numbers go in the path only as our own ids, never names or tax numbers.
Every method on one table
The Contracts table after each call. Watch which calls change it, and what a repeat does.
GET /api/contracts/42
→ 200 OK
Contracts (before the calls)
Id
Customer
Phone
Email
41
A. Rao
555-0101
rao@example.com
42
B. Ortiz
555-0142
ortiz@example.com
Result: 2 rows
Id
Customer
Phone
Email
41
A. Rao
555-0101
rao@example.com
42
B. Ortiz
555-0142
ortiz@example.com
1/7GET /contracts/42 reads row 42. The table doesn't change: safe.POST /contracts adds a row. The server picks the id, 43: 201 Created.The same POST again, by a retry: a second row, 44. POST is not idempotent.PUT /contracts/42 replaces the whole row. The email you left out is wiped. Twice: same row.PATCH /contracts/41 sends only a phone. The email stays.DELETE /contracts/44 removes the duplicate: 204. Again: 404, but the table is the same.GET changed nothing. PUT and DELETE repeat safely. POST made a duplicate.
The story
Before
HTTP is how programs ask each other for things: each request has a method (GET reads, POST sends) and each answer has a 3-digit status code (200 means it worked). JSON is the text format the data comes in. is Azure's monitoring tool; it counts failed requests by their status code. A retry is the client sending a request again after a timeout: harmless for a read, but it can repeat a change twice. A cache may serve an old saved answer. BMW FS's first contracts API, written years ago, put verbs in the address: POST /api/getContract,POST /api/updateContract,POST /api/deleteContract. Every answer was 200 OK, and an error was a field inside the JSON: "success": false. The dealer portal and two partner systems called it.
Dealer portalreads success
POST /getContractalways 200
200 OK
{ "success": false,
"error": "not found" }
1/4Before: every call is a POST to a verb, and every answer is 200, even errors.Problem: counts status codes. It sees 0% failures while dealers see errors.And nobody can tell a read from a change: a retry or a cache may repeat any of them.Fix: a resource address, a real HTTP method, a real status code. Monitoring sees the 404.
Why it had to change
Everything around an API reads the method and the status code: browsers, caches (saved copies of answers), retry policies (rules for sending a request again), load balancers (a front door that spreads requests over copies of the API) and monitoring (tools that count failures). If every call is a POST and every answer is 200, none of them can do their job, and every client must write its own error parsing.
How it works now
REST uses what HTTP already has. The controller is the class whose methods, called actions, receive the requests (). Here is one request and its answer, in the parts every tool understands:
Clientsends the request
APIthe controller
GET /api/contracts/42
1/4The request line says what and where: method GET, address/api/contracts/42.Headers carry extras, like the sign-in token. The API keeps no memory of the last call.The answer starts with the status code: 200 means it worked.Then the body, the resource as JSON. A tool that reads only the code still knows it worked.
What we gained
Retries, caches and monitoring work without custom code, and every client handles errors the same way. The rest of the card covers each method, idempotency, and the status codes to return.
In the lending API
Each marked line returns one status code; the diagram above shows the calls.
Only the key lines are shown; Deep walks the whole file.
Lending.sln · where this file sits
src/Api/Controllers/
⋯ 9 lines in Deep⋯ 26 lines in Deep
ContractsController.cs · line 20 · 1 of 4200 OK with the contract as JSON.
The code, file by file
The contracts controller in the BMW FS lending API. Tap Next to walk each method and the status code it returns.
Lending.sln · where these 2 files sit
src/Api/
Controllers/
Contracts/
Diagram · step 1GET reads contract 42. Nothing changes. The answer: 200 OK and the JSON.
Clientdealer portal
Contracts APIASP.NET Core
DatabaseContracts table
GET /api/contracts/42
read
200 OK + JSON
ContractsController.cs · line 1 · 1 of 20[ApiController]: invalid input gets an automatic 400, and routes must be attributes like the next line.
What they ask next
The follow-ups interviews really ask. Answer out loud, then tap to check.
ReportedWhat is the difference between POST, PUT and PATCH?
POST creates something new under a collection, and the server picks the id: two POSTs, two contracts. PUT sends the whole resource to its own address and replaces it: send it twice, same result, so it's idempotent. PATCH sends only the fields to change. Leaving a field out of a PUT means "clear it"; leaving it out of a PATCH means "don't touch it".
POST /api/contracts new: 201
PUT /api/contracts/42 replace
PATCH /api/contracts/42 a few fields
PUT body without phone: phone cleared
PATCH body without phone: phone kept
Sending the same request again leaves the server in the same state as sending it once. GET, HEAD, OPTIONS, PUT and DELETE are idempotent; POST is not; PATCH isn't required to be. Idempotent is about the state, not the answer: a second DELETE may get 404, but the contract is gone either way. Safe is stricter: GET, HEAD and OPTIONS never change anything.
safe idempotent
GET yes yes
PUT no yes
DELETE no yes
PATCH no not always
POST no no
ReportedWhich status codes do you use, and what do they mean?
2xx success: 200 OK, 201 Created with a header, 202 Accepted for work that finishes later, 204 No Content. 3xx redirects: 301, 302, and 304 Not Modified for caching. 4xx the client must change something: 400, 401, 403, 404, 405 method not allowed, 409 conflict, 415 wrong content type, 422 when the input is well-formed but breaks a rule (ASP.NET Core uses 400 for validation by default), 429 too many requests. 5xx the server failed: 500 our bug, 502 and 504 a dependency failed or was slow, 503 overloaded or down.
Answer, then open the next level. That is the order an interviewer digs in.
Reporteda candidate was asked Intervieweran interviewer asks it
ReportedWhat is the difference between POST, PUT and PATCH?
POST creates something new under a collection, and the server picks the id: two POSTs, two contracts. PUT sends the whole resource to its own address and replaces it: send it twice, same result, so it's idempotent. PATCH sends only the fields to change. Leaving a field out of a PUT means "clear it"; leaving it out of a PATCH means "don't touch it".
POST /api/contracts new: 201
PUT /api/contracts/42 replace
PATCH /api/contracts/42 a few fields
PUT body without phone: phone cleared
PATCH body without phone: phone kept
Yes, if the client chooses the address, like PUT /api/documents/2026-10-statement; the answer is then 201. Most of our APIs let the server pick ids, so creation is a POST.
How do you write PATCH in ASP.NET Core?
The simple way: a small class whose properties are nullable, and the service sets only the ones that aren't null. The standard way is JSON Patch (a list of operations like replace /phone), supported through a NuGet package. We use the simple way unless a client needs to clear a field explicitly.
What does idempotent mean, and which methods are?
Sending the same request again leaves the server in the same state as sending it once. GET, HEAD, OPTIONS, PUT and DELETE are idempotent; POST is not; PATCH isn't required to be. Idempotent is about the state, not the answer: a second DELETE may get 404, but the contract is gone either way. Safe is stricter: GET, HEAD and OPTIONS never change anything.
safe idempotent
GET yes yes
PUT no yes
DELETE no yes
PATCH no not always
POST no no
So how do you make a POST safe to retry?
An idempotency key. The client makes a unique id once per payment and sends it in an Idempotency-Key header on every try. The API stores the key with the result, in a table with a unique index; a repeat with the same key gets the stored answer and no second payment. The header is an IETF draft (a proposed internet standard, not yet final), already used by payment providers. The incident in Deep and the hard live task build it.
ReportedWhich status codes do you use, and what do they mean?
2xx success: 200 OK, 201 Created with a header, 202 Accepted for work that finishes later, 204 No Content. 3xx redirects: 301, 302, and 304 Not Modified for caching. 4xx the client must change something: 400, 401, 403, 404, 405 method not allowed, 409 conflict, 415 wrong content type, 422 when the input is well-formed but breaks a rule (ASP.NET Core uses 400 for validation by default), 429 too many requests. 5xx the server failed: 500 our bug, 502 and 504 a dependency failed or was slow, 503 overloaded or down.
401 Unauthorized really means "not authenticated": no token, or an expired one; sign in and try again. 403 Forbidden means we know who you are, and you may not do this. A dealer reading another dealer's contract is a 403, or a 404 if we don't want to admit it exists.
Which ones should a client retry?
5xx, 408 Request Timeout and 429 Too Many Requests (after the Retry-After time, a header that says how long to wait). Not other 4xx: the same request will fail the same way. And only idempotent requests, unless they carry an idempotency key.
ReportedIs POST more secure than GET?
Over HTTPS both are encrypted on the wire, the query string too. The difference is where the data ends up: a GET's URL is saved in browser history, server and proxy logs and analytics, so personal data or tokens in a URL leak. That is why sensitive data goes in a POST body or a header, and GETs carry only ids. GET also may be cached and must not change anything.
Roy Fielding's six constraints: client-server, stateless, cacheable responses, a uniform interface (resources with addresses, standard methods, self-describing messages), a layered system (clients can't tell a gateway from the server), and optionally code on demand (the server sends code, like scripts, for the client to run). In practice most "REST" APIs do resources, methods and status codes, and skip links in responses that tell the client what it can do next (HATEOAS). REST is a style, not a framework or a standard.
Is a REST API stateless? Why does that help scale?
Yes: the server keeps no session between requests; each one carries its token and data. So any copy of the API can answer any request, and we can add copies behind the load balancer or restart one without logging anyone out. State lives in the database or a cache, not in the server's memory ().
ReportedHow does a REST API work inside ASP.NET Core?
Kestrel receives the HTTP request, the middleware pipeline runs (errors, auth), routing matches the method and address to a controller action, model binding reads the route values and the JSON body, the action calls a service, and its result is written as JSON with the status code. The page walks it, and the covers the controller part.
HTTPS only; a token on every request (usually a JWT, a signed token that says who you are), checked by the authentication middleware; an authorization check on every resource, not just "signed in" (can this dealer see contract 42?); validated input; rate limits; no secrets or personal data in URLs or logs. The JWT, authorization and API-security cards cover each one.
Most often in the path, /api/v2/contracts, because it is visible in logs and easy to route; sometimes a header or query string. The package handles it in ASP.NET Core. The rule that matters more: never break a published version. Add optional fields freely; removing or renaming one needs a new version.
Production bugs, and how to tell them
Each one: what you see, why, the fix in code, and the line to say.
Partner system showed blank contracts for a week
Navistar dealer system, contract lookup:
200 OK
{ "success": false,
"error": "Contract not found" }
App Insights: failed requests 0%
Cause
The lookup action caught "not found" and returned Ok(new { success = false, error = ... }). The status code said success, so the dealer system's client parsed the body as a contract and showed an empty one, and , which counts failures by status code, saw none.
Fix
Return : a 404 with a ProblemDetails body. The dealer system shows "not found", and the failures are visible in monitoring. A test asserts the status code, not the body text.
Diagram · step 1Before: an unknown contract number. The action answers 200 with an error inside.
Dealer systempartner client
Lookup actionour API
App Insightscounts failures
GET /contracts/99
200 {success:false}
Before · ContractsController.cs · line 3 · 1 of 1200 OK, whatever is inside. new { ... } is a throw-away object with just these two fields. Every tool reads the 200 as success.
Say it
We returned 200 with an error inside, so clients and monitoring both thought it worked. Now errors use the real status code with a ProblemDetails body, and the tests check the status code.
Customers got the same statement email three times
BMW FS customer emails, after a slow
morning on the mail service:
"I received my statement 3 times."
(Mail service log: 3 sends per customer)
Cause
The portal's back end called GET /api/contracts/{id}/send-statement to email a statement. GET is meant to be safe, so the retry policy retried it freely: when the mail service was slow, the first call timed out after the email had been sent, and the retries sent it again.
Fix
Sending an email changes something, so it is a POST: POST /api/contracts/{id}/statement-emails, which answers 202 Accepted and queues the email once. The client's resilience handler calls , because by default it retries POSTs too. GETs only read, so retrying them is harmless again.
Diagram · step 1Before: a GET sends the email. The mail service is slow; the email goes, the answer is late.
Portal back endHttpClient + retries
Contracts APIstatement
Mail servicesends emails
GET send-statement
send #1
Before · StatementsController.cs · line 1 · 1 of 2A GET that changes something: it sends an email.
Say it
We had a GET that sent emails, and retry policies treat GETs as safe to repeat. Anything that changes the world is a POST, with retries turned off for unsafe methods, and GETs only read, so retries can't cause side effects.
Production incident: A retried payment POST charged 318 customers twice
P1 · outage or data leakBMW FSanother system failedPayments API, called by the Customer API (both .NET on App Service)
Composed, not a real event at BMW FS. The mechanism is real: the .NET standard resilience handler retries every HTTP method by default, POST included, after its 10-second attempt timeout (Microsoft Learn, "Build resilient HTTP apps"). Times, counts, names, ticket and PR numbers are composed.
The story in 5 lines
On Wed 14 Oct, BMW FS's afternoon reconciliation flagged 318 customers who had paid the same amount two or three times within seconds through the customer portal's Pay now button.
Between 11:40 and 12:25 ET, was slow during a large index rebuild, and some payment POSTs to the Payments API took 12 to 15 seconds.
The Customer API's used the standard resilience handler. Its attempt timeout is 10 seconds and it retries every method by default, POST included. Each slow first try still finished, and each retry made another payment.
Payments operations pulled the duplicates from the 18:00 file (the bank debit file), so no customer was charged twice. turned off retries for POST in config at 14:30.
The fix: every payment POST carries an Idempotency-Key that the Payments API stores with a unique index, and clients never retry unsafe methods.
Who's who (5 people)
You
.NET developer on the vendor team (L3). Owns the fix.
Tech lead
Vendor senior. Runs the bridge call, reviews every PR.
L2 pod
Vendor support. Runs queries, changes config with approval.
BMW FS payments operations lead
Owns the daily ACH file to the bank. Decides what is pulled.
BMW FS IT change manager
Approves the emergency config change and the fix's release.
Tap a step. The map lights the place where it happens.
Customer portalPay now button
Customer APIHttpClient + retries
Payments APIPOST payments
Azure SQLPayments table
ACH filebank debits, 18:00
App Insightsrequests, KQL
1/8 Detect
Reconciliation flags 318 customers with two or three identical payments today.
Wed 14 Oct, 14:05 ET (23:35 IST)BMW FS payments operations, then the L2 pod
What: the 14:00 reconciliation report, which compares payments with the day's bank file, flags 318 customers with two or three payments of the same amount, seconds apart.
Why it hid: every payment request got a 201. The API logged no errors, because from its side every request was a valid new payment.
How we saw it: grouped today's payments by contract and amount in . The duplicates were all between 11:40 and 12:25 ET.
App Insights: payment POSTs per contract
requests
| where timestamp > startofday(now())
| where cloud_RoleName == "payments-api"
| where name == "POST Payments/Pay"
| extend contract = tostring(
customDimensions.ContractId)
| summarize tries = count(),
first = min(timestamp)
by contract
| where tries > 1
Try it yourself · 20 minutes, on your Mac
dotnet new webapi -n RetryLab --use-controllers. Add a POST action that waits 12 seconds, adds a row to a static list and returns 201.
In a console app, add and register an with AddStandardResilienceHandler().
POST once. Then GET the list from the API: two or more rows, from one call.
Add o.Retry.DisableForUnsafeHttpMethods() and repeat: one row, and a timeout on the client.
You should see
A client timeout doesn't stop the server. Retrying a POST without an idempotency key makes a second one.
The senior move
"Retries are only safe for idempotent requests. Money-moving POSTs carry an idempotency key the server stores with a unique index, and clients never retry unsafe methods on their own."
SourcesSourced: the standard resilience handler's defaults (10 s attempt timeout, 3 retries, all methods) and (Microsoft Learn, "Build resilient HTTP apps"); idempotent and safe methods (RFC 9110); the Idempotency-Key header (IETF draft). Status codes checked with a real ASP.NET Core run on .NET 10. Composed: BMW FS's systems, dates, counts, names, ticket and numbers, the and the SQL.
Traps
Don't sayPUT and PATCH are the same: both update.SayPUT replaces the whole resource; PATCH changes only the fields sent. A field missing from a PUT gets cleared.
Don't sayReturn 200 and put the error in the body.SayUse the real status code with a ProblemDetails body. Monitoring, retries and clients read the code.
Don't sayPOST is more secure than GET.SayBoth are encrypted over HTTPS. GET URLs end up in logs and history, so no personal data in them.
Don't say401 means you're not allowed.Say401 means not authenticated. Signed in but not allowed is 403.
Don't sayRetries are always safe.SayOnly for idempotent requests, or a POST carrying an idempotency key.
Traps
What should you say instead? Tap to check.
Don't sayPUT and PATCH are the same: both update.
Say instead
PUT replaces the whole resource; PATCH changes only the fields sent. A field missing from a PUT gets cleared.
Don't sayReturn 200 and put the error in the body.
Say instead
Use the real status code with a ProblemDetails body. Monitoring, retries and clients read the code.
Don't sayPOST is more secure than GET.
Say instead
Both are encrypted over HTTPS. GET URLs end up in logs and history, so no personal data in them.
Don't say401 means you're not allowed.
Say instead
401 means not authenticated. Signed in but not allowed is 403.
Don't sayRetries are always safe.
Say instead
Only for idempotent requests, or a POST carrying an idempotency key.
Live coding
Easy first, then harder. Set a timer and type it before you look.
Task 1
Question index: Musteasy · 10 minutes
Design the endpoints for a contract's payments: list them, read one, make one, cancel one. For each, give the method, the address and the status codes it can return.
What does PUT promise about fields that are missing?
How can the PATCH action tell "not sent" from "sent"?
Which status code fits when nothing comes back?
Show a solution
Lending.sln · where these 2 files sit
src/
Api/Controllers/
Application/Contacts/
Diagram · step 2With PUT, the body is the whole contact. No email in it, so the email is cleared.
Clientsends phone only
APIPUT or PATCH
Contactphone · email
555-0142 · ben@example.com
ContactsController.cs · line 3 · 1 of 2PUT: body is the whole contact. A missing email arrives as null. The service below does the copying.
What the interviewer checks
PUT replaces every field: a missing email becomes empty.
PATCH with nullable properties sets only what was sent.
204 No Content for both; 404 for an unknown contract.
Can say which is idempotent and why.
Task 3
Question index: Musthard · 30 minutes
Make POST /api/contracts/{id}/payments idempotent with an Idempotency-Key header: store the key with the response, return the stored response for a repeat, and handle two copies of the same request arriving at the same moment.
Where does the key live so every copy of the API sees it?
What stops two requests with the same key from both inserting?
What should a repeat get back: a new answer or the first one?
Show a solution
Lending.sln · where these 2 files sit
src/Payments/
Controllers/
Idempotency/
Diagram · step 1The client makes key 7f3a… once, and sends it with the payment POST.
Customer servicecalls Payments; retries
Payments APIthe action
Azure SQLIdempotencyKeys
POST + Idempotency-Key
PaymentsController.cs · line 4 · 1 of 5Reads the key from the request header.
What the interviewer checks
400 when the header is missing on a money-moving POST.
Key stored in the database with a unique index, not in memory.
Insert the key first; a duplicate-key error means another request owns it.
A repeat returns the first response's status and body.
Says the key is made by the client once per payment, not per try.
Senior extra: the key row has an "in progress" status with a time limit, so if the first try crashes, a repeat isn't left waiting forever.
With AI tools
Interviewers now ask how you check AI-written code. Assistants generate controllers quickly, and two habits slip in: returning with an error object inside, and wrapping every in a retry policy without asking which calls are POSTs. I check every generated action's status codes against the list on this card, and every retry policy for or an idempotency key.