Contract thinking from the caller's side
Design an interface from its caller's code: name each choice, report errors as next moves, and promise explicitly what callers may rely on.
Predict first
Before you read: predict
A payments ledger documents GET /accounts/:id/entries as "Returns the account's entries, each { id, postedAt, amountCents }." Neither its documentation nor its schema mentions an order. Today it returns them as they were posted:
[{ "id": "e-501", "postedAt": "2026-09-01T09:00:00Z", "amountCents": 1200 },
{ "id": "e-517", "postedAt": "2026-09-14T09:00:00Z", "amountCents": 4500 },
{ "id": "e-533", "postedAt": "2026-09-28T09:00:00Z", "amountCents": 2500 }]
A partner's nightly reconciliation job, which your team does not own, ends with:
Three merge requests are proposed for the endpoint.
Which one changes the checkpoint this job saves for the account?
const entries = await getEntries(accountId);saveCheckpoint(entries.at(-1).id); // the next run starts after this entryThe 30-second version
Write the caller's code before the signature. A boolean argument hides its meaning at the call: name each choice, accept a boolean the caller already holds, derive what the caller should not choose. Make each error name the caller's next move. Callers depend on what their code observes, promised or not: promise explicitly what they may rely on.
Design an interface from its caller's code: name each choice, report errors as next moves, and promise explicitly what callers may rely on.
How it works
Step 1 of 13
In the kitchen
- the instruction card ("to heat the water for an hour, press Boost") is written before the panel → the caller's code first (step 1)
Analogy: Designing a home boiler controller's front panel (appliance design)
A heating-controls maker designs the panel a homeowner uses every day.
| At the controls maker | In the interface |
|---|---|
| the instruction card ("to heat the water for an hour, press Boost") is written before the panel | the caller's code first (step 1) |
| a row of unlabelled 0/1 switches becomes buttons named Boost and Holiday | named choices, not flags (steps 2–3) |
| the room thermostat sends on or off, so the boiler takes that signal as it is | a boolean the caller already holds (step 4) |
| "Pressure low: top up to 1.5 bar at the filling loop" instead of "E37" | an error that names the next move (steps 7–8) |
| the old model always came back with the heating on after a power cut; nobody wrote it down, and owners of empty winter houses relied on it | Hyrum's Law (steps 9–10) |
| the next model restarts with everything off, and their pipes freeze | an unpromised change breaks (step 10) |
| the maker writes the restart behaviour into the manual and keeps it | promise explicitly (step 12) |
Breaks down when: a homeowner can phone the maker and be talked through a new panel, while a caller's code fails silently in someone else's job at night. Callers you do not know exist cannot be warned.
Contrasting case: a boiler's internal pump speed, which no homeowner can see, hear or feel. A behaviour no caller can observe can change freely; the law is about what callers observe.
In real terms: write the caller's code first, name its choices, report failures as its next moves, and treat what it can observe as promised.
Examples, run for real
4.1 ExamHall EXH-2480: "latest" moves from attempt 243 to 187
GET /candidates/:id/attempts never promised an order; it happened to return attempts oldest first. The proctor console's "latest attempt" tile read attempts.at(-1).id. A refactor reused the leaderboard query, highest score first. fixtures/hyrum.mjs, run unchanged:
4.1 node fixtures/hyrum.mjs (unchanged copy, sha256 19ac50873f4f)
4.1 v1 latest: 243
4.1 v2 latest: 187
4.1 v3 latest (explicit field): 243
No field, type or documented promise changed, and the tile now names attempt 187, an older one (steps 9–10). MR !2780 adds latestAttemptId to the response, documents it and points the tile at it (step 12). Callers still reading position keep the old risk until they move (step 13).
4.2 ExamHall EXH-2481: startExam(id, true, false)
startExam(attemptId, proctored, timed) takes two booleans. A new call site swapped them. MR !2781 replaces both with named options that refuse a key the function does not know:
4.2 startExam('a-243', true, false) -> proctored yes · timed no · no error
4.2 startExam('a-243', false, true), the two swapped -> proctored no · timed yes · no error
4.2 startExam('a-243', { proctored: true, timed: false }) -> proctored yes · timed no · no error
4.2 startExam('a-243', { timed: false, proctored: true }) -> proctored yes · timed no · no error
4.2 startExam('a-243', { proctord: true }), misspelt -> throws TypeError: unknown option "proctord"
The swapped call runs without an error, as an unproctored, timed sitting (step 2). With named options the order of the keys does not matter, and a misspelt key stops at the call (step 3).
Try it
Answer first, then check. Nothing is saved.
Question 1 of 6
A merge request adds a line to a payments ledger's nightly payout job: settle(batch.id, true). The function is settle(batchId, instant): true pays out through the instant-transfer rail and false through the standard one, and its body tests instant in four places. The job always pays out instantly.
Which review comment fits published guidance on API design?
Question 2 of 6
A ticket kiosk's team wrote its failure handling before the booking API existed, and the kiosk ships with it:
The booking API can fail in four ways: (1) another buyer held the seat a moment earlier, (2) the seat-map service timed out, (3) ticket sales for the event have closed, (4) the seat number is not on the event's map.
Which error design sends each failure to the one kiosk move that can succeed for it?
try { await holdSeat(eventId, seat); }catch (err) { if (err.retryable) return resendNow(); // the same request again if (err.field === 'seat') return askForAnotherSeat(); // this seat cannot be had return sendToHelpDesk();}Question 3 of 6
A streaming app's settings API needs an autoplay switch with two callers. The settings screen has an Autoplay check-box and passes on whatever it shows. The code that creates a children's profile always turns autoplay off.
Which API fits published guidance on designing for the caller?
Question 4 of 6
A chat platform documents its rate-limit error as { status: 429, message }, with no list of message texts. A release reworded the message from "Rate limit exceeded" to "Too many requests, slow down", and every schema and contract test passed. A customer's support bot, which waited and resent a message only when the text contained "Rate limit", has dropped such messages since the release.
In the incident review, which position fits published guidance on changing an API that many callers use?
Question 5 of 6
A streaming service's GET /users/:id/history?page=N returns 20 watched episodes a page. Its documentation states no order; today each page comes newest first. The TV app, released once a month, shows the first entry of page 1 as "Continue watching". A new "Most watched" shelf on the website needs the user's 10 most-played episodes now, and the next TV release is three weeks away.
For a user who has watched 45 episodes, several of the most played months ago, which change ships the shelf and keeps the TV app's "Continue watching" right until then?
Question 6 of 6
A payments service is adding a refund routine. Each payment has a refundPreference, 'card' or 'credit', which the payer ticked at checkout. Refunds are made on a support agent's screen with two buttons, "Refund to card" and "Give store credit": on a call, the agent presses the one the customer asks for, and each button's handler has its destination written in its code.
Which signature fits published guidance on flag arguments?
Go deeper
Pro tips
- In test and staging builds, return any list whose order you have not promised in a seeded shuffled order, so a caller reading position fails before production does. When: an endpoint returns a list and its documentation states no order (steps 9–10). Status: RAN.
Source
Quotes are verbatim, with line breaks joined. Valid as of 2026-10-02.
- Hyrum Wright, Hyrum's Law: https://www.hyrumslaw.com/ (
hyrums-law.txt). The row's first source. Quoted: "With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody."; "This effect serves to constrain changes to the implementation, which must now conform to both the explicitly documented interface, as well as the implicit interface captured by usage."; "The creation of the implicit interface usually happens gradually, and interface consumers generally aren’t aware as it’s happening."; "With a bit of luck, widespread, comprehensive, and automated testing can detect these new expectations but not ameliorate them." - Martin Fowler, Flag Argument (bliki), 23 June 2011: https://martinfowler.com/bliki/FlagArgument.html (
fowler-flag-argument.txt). The row's second source. Quoted: "the separate methods communicate more clearly what my intention is when I make the call"; "In this case one option is to retain the method with the flag argument, but keep it hidden."; "then the caller has no business caring about the difference between premium and regular booking"; "it's perfectly reasonable for the booking routine to derive it's true method based on the customer status"; "an API should be written to make it easier for the caller, so if we know where the caller is coming from we should design the API with that information in mind"; "This also argues that we may sometimes provide both styles if we get callers in both ways."
Related lessons
- Read first: HTTP method semantics, safe, idempotent, PUT vs PATCH vs POST
- Read first: Data first: model the data, the rest follows
- Related: Idempotency keys for unsafe operations
- Related: API contract from the client's side
- Related: Pagination, offset vs keyset/cursor, with filtering and sorting
- Related: SOLID at a glance, with LSP and ISP in TypeScript
- Related: Feature flags and their lifecycle
Checked against the tools and docs as of 2026-10-02.