Skip to content
SparkTime, home
System Design From Zero to Interview-Ready
Lessons
1 · System Design Foundations
2 · APIs and Communication
3 · Capacity Estimation
4 · Core Building Blocks
5 · Databases
6 · Distributed Systems
7 · Reliability and Operations
8 · Architecture and Security
9 · Interview Method and Foundation Case Studies
10 · Real-World Case Studies
11 · Large-Scale Case Studies
12 · Interview Readiness
Lesson 4 of 45

Lesson 4 · APIs and Communication

API Design for System Design: REST, Status Codes, Pagination and Versioning

Design APIs that clients can rely on: resources and URLs, HTTP methods and which are safe to retry, status codes, validation, error formats, cursor pagination, filtering, idempotency keys, rate-limit responses, versioning, and when gRPC or GraphQL fit better than REST.

On this page

Key takeaways

  • An API is a contract: design it around resources and the operations clients need, and change it only in backward-compatible ways.
  • Know which methods are safe and idempotent; make non-idempotent operations such as payments retry-safe with an idempotency key.
  • Use status codes precisely, return a consistent error format, and validate input at the boundary.
  • Prefer cursor pagination over offsets for large or changing lists, and cap page sizes.
  • REST is a good default for public and CRUD-style APIs; gRPC suits internal service-to-service calls; GraphQL suits clients that need flexible data shapes.

An API is a promise. Once a mobile app ships that calls GET /photos/123 and expects a field called caption, you cannot rename that field without breaking every phone that has not updated. Databases can be migrated in a night; installed clients cannot. That is why the API is one of the few parts of a design that is expensive to change, and why interviewers ask you to define it before you draw the architecture.

In the seven-phase method this is phase 3, Interface. This article teaches you to design APIs that are clear, safe to retry, and able to evolve.

What you will learn

#
  • What REST means in practice: resources, representations and URLs.
  • Which HTTP methods are safe and idempotent, and why that matters for retries.
  • Which status codes to use, and a consistent error format.
  • How to validate requests at the boundary.
  • Offset versus cursor pagination, and why cursors win for large lists.
  • Filtering, sorting, authentication, rate-limit responses and idempotency keys.
  • How to version an API without breaking clients.
  • When gRPC or GraphQL is a better fit than REST.

Why the API comes before the architecture

#

The API defines what the system must do from the outside. Once you know the operations (create a photo, read a feed, like a photo), you know the read and write paths you must design, and their shapes tell you a lot about the data model. A good API definition in an interview takes three or four minutes and saves you from designing machinery for operations nobody needs.

There are two kinds of API in most systems:

  • External APIs used by browsers, mobile apps and partners. They change slowly and must be backward compatible.
  • Internal APIs between your own services. They can change faster, but still need care, because services are deployed at different times.

REST in plain words

#

REST (representational state transfer) is a style for designing HTTP APIs. In practice it means:

  1. Model the API as resources (nouns): users, photos, comments, orders.
  2. Give each resource a URL: /photos/123.
  3. Use HTTP methods for the operation: GET to read, POST to create, PUT or PATCH to update, DELETE to remove.
  4. Exchange representations, usually JSON, of the resource's state.
  5. Keep requests stateless: each request carries what the server needs, such as an authentication token.

URL design

#

Good

Avoid

Why

GET /photos/123

GET /getPhoto?id=123

The method already says "get"

POST /photos

POST /createPhoto

Create by posting to the collection

GET /users/42/photos

GET /photosOfUser/42

Nested resources show the relationship

POST /photos/123/likes

POST /likePhoto/123

A like is a resource you create

GET /photos?owner=42&sort=-created_at

Separate endpoint per filter

Filters and sorting as query parameters

Keep nesting shallow. /users/42/photos/123/comments/9 is hard to use; /comments/9 is usually enough once a comment has its own id.

Not everything maps neatly to a noun. Actions such as "cancel an order" are often modelled as a state change (PATCH /orders/77 with {"status": "cancelled"}) or as a sub-resource (POST /orders/77/cancellation). Both are fine; be consistent.

HTTP methods: safe, idempotent, neither

#

RFC 9110 defines two properties that matter a great deal in distributed systems (HTTP Semantics):

  • A safe method is read-only: calling it should not change server state.
  • An idempotent method can be called several times with the same effect as calling it once.

Method

Typical use

Safe?

Idempotent?

GET

Read a resource or list

Yes

Yes

HEAD

Read headers only

Yes

Yes

PUT

Replace a resource at a known URL

No

Yes

DELETE

Remove a resource

No

Yes

POST

Create a resource, or trigger an action

No

No

PATCH

Partially update a resource

No

Not necessarily

Why idempotency matters so early

#

Networks fail in an annoying way: a client sends a request, and the connection drops before the response arrives. Did the server process it? The client cannot know. The safe reaction is to retry. For GET, PUT and DELETE that is harmless: reading twice, setting the same value twice, or deleting something already deleted leaves the same end state.

For POST /payments, a retry might charge the customer twice. The standard fix is an idempotency key: the client generates a unique id for the operation and sends it in a header. The server remembers the key and its result; if the same key arrives again, it returns the stored result instead of repeating the operation.

Code
POST /paymentsIdempotency-Key: 6f1c2a9e-1b0d-4c55-9a4e-2f1a7d0c3b88Content-Type: application/json{"order_id": "ord_77", "amount": 49900, "currency": "INR"}

This pattern is widely used by payment providers. Stripe's documentation, for example, describes saving the status code and body of the first request for a key and returning the same result for retries, comparing parameters to detect misuse, and allowing keys to be pruned after at least 24 hours (Idempotent requests). The IETF has worked on standardising an Idempotency-Key request header; as of October 2026 the draft is listed as expired and is not an RFC (Idempotency-Key header draft), so treat the header name as a common convention rather than a standard.

Article 20 covers how to build this safely. For now: whenever an API creates something important or moves money, ask how a retry is made safe.

Status codes you actually need

#

Code

Meaning

Use it when

200 OK

Success with a body

Reads, and updates that return the resource

201 Created

A resource was created

POST that creates; include a Location header with the new URL

202 Accepted

Accepted for later processing

Long-running work handed to a background job (article 05)

204 No Content

Success, no body

DELETE, or updates that return nothing

301, 308

Moved permanently

A URL has a new permanent home; clients and caches may remember it

302, 307

Found / temporary redirect

Temporary redirects; the URL shortener in article 30 discusses which to use

304 Not Modified

Cached copy is still valid

Conditional GET with If-None-Match

400 Bad Request

Malformed request

Invalid JSON, wrong types, missing required fields

401 Unauthorized

Not authenticated

Missing or invalid credentials

403 Forbidden

Authenticated but not allowed

User may not access this resource

404 Not Found

No such resource

Also used to avoid revealing that a private resource exists

409 Conflict

Conflicts with current state

Username taken; booking a seat already booked

412 Precondition Failed

Conditional update failed

If-Match version check failed (optimistic concurrency, article 14)

422 Unprocessable Content

Well-formed but semantically invalid

Date range where the end is before the start

429 Too Many Requests

Rate limited

Include Retry-After when you can (RFC 6585)

500 Internal Server Error

Unexpected server failure

Bugs; never for client mistakes

502 Bad Gateway, 504 Gateway Timeout

An upstream failed or timed out

Usually produced by proxies and load balancers

503 Service Unavailable

Temporarily unable to serve

Overload or maintenance; may include Retry-After

The split between 4xx and 5xx matters operationally. A 4xx means the client must change something; retrying the same request will fail again. A 5xx means the server had a problem; a retry later might succeed. Clients, load balancers and alerting all rely on this difference.

A consistent error format

#

Return errors in one predictable shape so clients can handle them in one place. RFC 9457 defines a standard JSON format called "problem details" (Problem Details for HTTP APIs):

JSON
{  "type": "https://api.pixly.example/problems/caption-too-long",  "title": "Caption too long",  "status": 422,  "detail": "Captions can be at most 2,200 characters; this one has 2,512.",  "instance": "/photos"}

Never put stack traces, SQL or internal hostnames in error responses. They help attackers more than users.

Validate at the boundary

#

Every request from outside is untrusted. Validate it as soon as it arrives, before it reaches business logic or the database:

  • Shape and types: required fields present, numbers are numbers, ids match the expected format.
  • Size limits: maximum body size, maximum string lengths, maximum list lengths, maximum page size. Unbounded inputs are a classic way to exhaust memory.
  • Ranges and enums: quantities positive, status values from a known list, dates in a sensible range.
  • Authorisation: the caller may act on this resource, not just on resources of this type. OWASP lists broken object-level authorisation as the top API security risk (article 28).

Rejecting bad input early with a clear 400 or 422 is cheaper for everyone than failing deep inside the system.

Pagination: offset versus cursor

#

Any endpoint that returns a list needs pagination. Returning 50,000 photos in one response would be slow, memory-hungry and useless to a phone screen.

Offset pagination

#
Code
GET /users/42/photos?limit=20&offset=40

The server runs something like "skip 40 rows, return the next 20". It is easy to understand and allows jumping to page 7. It has two problems:

  1. It gets slower as the offset grows. Most databases must still walk past the skipped rows. Page 5,000 of a large table can be very slow.
  2. It is unstable when data changes. If a new photo is added at the top between requests, every item shifts down by one. The client sees one photo twice. If one is deleted, the client misses one.

Cursor (keyset) pagination

#
Code
GET /users/42/photos?limit=20-> {"items": [...], "next_cursor": "aWQ6OA"}GET /users/42/photos?limit=20&cursor=aWQ6OA

The server returns a cursor that encodes the position of the last item (for example its creation time and id). The next request asks for "items after this position", which an index can answer directly, however deep you are. New items at the top do not shift the next page.

The cursor should be opaque to clients (for example, base64-encoded), so you can change its internal format later without breaking them.

Seeing the difference

#

This small Java program simulates both approaches on a list of posts ordered newest first, with a new post arriving between page 1 and page 2. It is a model of the idea, not a web service; read it as pseudocode if you prefer another language.

Java
// File: PaginationDemo.javaimport java.nio.charset.StandardCharsets;import java.util.ArrayList;import java.util.Base64;import java.util.List;public class PaginationDemo {    // Posts are identified by increasing ids; a higher id means newer. The feed shows newest first.    static List<Integer> posts = new ArrayList<>();    static List<Integer> offsetPage(int offset, int limit) {        List<Integer> page = new ArrayList<>();        for (int i = offset; i < Math.min(offset + limit, posts.size()); i++) {            page.add(posts.get(i));        }        return page;    }    // Returns the ids strictly older than the cursor position.    static List<Integer> cursorPage(String cursor, int limit) {        int after = cursor == null ? Integer.MAX_VALUE : decode(cursor);        List<Integer> page = new ArrayList<>();        for (int id : posts) {            if (id < after && page.size() < limit) {                page.add(id);            }        }        return page;    }    static String encode(int id) {        return Base64.getUrlEncoder().withoutPadding()                .encodeToString(("id:" + id).getBytes(StandardCharsets.UTF_8));    }    static int decode(String cursor) {        String raw = new String(Base64.getUrlDecoder().decode(cursor), StandardCharsets.UTF_8);        return Integer.parseInt(raw.substring(3));    }    static void reset() {        posts.clear();        for (int id = 10; id >= 1; id--) {            posts.add(id);        }    }    public static void main(String[] args) {        reset();        System.out.println("Offset page 1: " + offsetPage(0, 3));        posts.add(0, 11); // a new post arrives at the top        System.out.println("Offset page 2: " + offsetPage(3, 3) + "  <- 8 is shown twice");        reset();        List<Integer> first = cursorPage(null, 3);        String next = encode(first.get(first.size() - 1));        System.out.println("Cursor page 1: " + first + " next=" + next);        posts.add(0, 11);        List<Integer> second = cursorPage(next, 3);        System.out.println("Cursor page 2: " + second + " next=" + encode(second.get(second.size() - 1)));    }}
Code
Offset page 1: [10, 9, 8]Offset page 2: [8, 7, 6]  <- 8 is shown twiceCursor page 1: [10, 9, 8] next=aWQ6OACursor page 2: [7, 6, 5] next=aWQ6NQ

The offset version repeats post 8 because everything shifted down. The cursor version continues exactly where it stopped. In a real database, the cursor query is a range scan on an index such as (owner_id, created_at, id), so its cost does not grow with page depth (article 13 explains why).

Offset

Cursor

Jump to page N

Yes

No (only next and previous)

Cost of deep pages

Grows with the offset

Constant with a suitable index

Stable under inserts and deletes

No

Yes

Typical use

Small admin tables, search results with page numbers

Feeds, timelines, infinite scroll, large exports

Always cap the page size (for example a maximum of 100) so one request cannot ask for a million rows.

Filtering, sorting and field selection

#
  • Filtering with query parameters: GET /orders?status=shipped&created_after=2026-09-01.
  • Sorting with a parameter: sort=-created_at (minus for descending). Allow sorting only on fields you have indexed, or a single request can trigger a full table scan.
  • Field selection (optional): fields=id,caption,thumbnail_url lets mobile clients fetch less data.

Each filter and sort option is a promise to support a query efficiently. Offer the ones clients need, not every possible combination.

Authentication, rate limits and other cross-cutting concerns

#

These apply to almost every endpoint, so decide them once:

  • Authentication. External APIs commonly use bearer tokens in the Authorization header; partner APIs often use API keys. Article 28 compares sessions, tokens and JWTs.
  • Rate limits. Protect the system and share capacity fairly. Respond with 429 Too Many Requests and, when you can, a Retry-After header. Article 31 designs a rate limiter.
  • Request ids. Accept or generate a request id and return it in a header, so a user's error report can be traced through logs (article 25).
  • Caching headers. Cache-Control and ETag let clients and CDNs avoid refetching unchanged data (articles 09 and 10).

Conditional requests and optimistic concurrency

#

HTTP has a built-in way to avoid two clients overwriting each other's changes. The server returns a version tag (ETag) with a resource. A client that updates the resource sends If-Match with the tag it read. If someone else changed the resource in the meantime, the tag no longer matches and the server returns 412 Precondition Failed instead of silently overwriting. Article 14 explains this "optimistic concurrency" pattern in the database.

Long-running operations

#

If an operation takes longer than a few seconds (generating a report, transcoding a video), do not hold the HTTP request open. Return 202 Accepted with a URL where the client can check the status:

Code
POST /exports            -> 202 Accepted, Location: /exports/exp_31GET  /exports/exp_31     -> 200 {"status": "running", "progress": 40}GET  /exports/exp_31     -> 200 {"status": "done", "download_url": "..."}

Article 05 covers this asynchronous pattern and its alternatives.

Versioning and backward compatibility

#

APIs evolve. The goal is to change them without breaking existing clients.

Backward-compatible changes (safe to make in place):

  • adding a new endpoint;
  • adding an optional request field;
  • adding a field to a response (clients must ignore fields they do not know);
  • adding a new value to an enum, if clients were told to handle unknown values.

Breaking changes (need a new version or a migration plan):

  • removing or renaming a field;
  • changing a field's type or meaning;
  • making an optional field required;
  • changing status codes or error formats that clients rely on.

Common ways to express versions:

Style

Example

Notes

URL path

/v1/photos

Very visible and easy to route; the most common for public APIs

Header

Accept: application/vnd.pixly.v2+json or a custom version header

Keeps URLs clean; less visible

Query parameter

/photos?api-version=2026-09-01

Simple; some providers use dates as versions

Whatever you choose, the harder part is process: announce deprecations, monitor which clients still use old versions, and keep old versions running long enough for mobile apps to update. Microsoft's API design guidance covers these practices in more depth (Web API design best practices).

Beyond REST: gRPC, GraphQL and real-time APIs

#

REST is a good default, not the only option.

Style

How it works

Strengths

Weaknesses

Typical use

REST over HTTP + JSON

Resources, methods, status codes

Universal, simple, cache-friendly, easy to debug

Can be chatty; over-fetching or under-fetching

Public APIs, web and mobile backends

gRPC

Remote procedure calls defined in a schema (Protocol Buffers), binary encoding, over HTTP/2

Compact and fast, strong typed contracts, streaming

Harder to call from browsers directly, less human-readable

Internal service-to-service calls

GraphQL

One endpoint; the client sends a query naming exactly the fields it wants

Flexible for many client types, avoids over-fetching

HTTP caching is harder; expensive queries must be limited; resolvers can cause many database calls

Products with many screens and clients needing different shapes

WebSockets / Server-Sent Events

Long-lived connection; server pushes data

Real-time updates

Stateful connections are harder to scale

Chat, live scores, notifications (article 05)

A reasonable interview position: "REST for the public API, gRPC between internal services if latency and strict contracts matter, and WebSockets for the real-time channel." Then justify each.

Worked example: Pixly's API

#

Here is how the Interface phase looks for Pixly. In an interview you would write the first column and talk through the rest.

Endpoint

Purpose

Success

Notable errors

POST /v1/photos

Start an upload: send caption and metadata, receive an upload URL (article 10)

201 with photo id and upload URL

400 invalid, 413 too large, 429 rate limited

GET /v1/photos/{photoId}

Read one photo's metadata

200

404 not found or not visible

DELETE /v1/photos/{photoId}

Delete own photo

204

403 not owner

GET /v1/feed?cursor=...&limit=20

Home feed, newest first

200 with items and next_cursor

401 not signed in

PUT /v1/photos/{photoId}/likes/me

Like a photo

204

404

DELETE /v1/photos/{photoId}/likes/me

Unlike

204

404

PUT /v1/users/{userId}/followers/me

Follow a user

204

404

Notice two deliberate choices:

  • Likes and follows use PUT on a resource that represents "my like", which makes them naturally idempotent. Liking twice leaves you with one like, and retries are harmless. A POST /likes design would need extra deduplication.
  • The feed uses cursor pagination, because it is a large, constantly changing list read with infinite scroll.

The same API in a Spring-style controller

#

This fragment shows how such an API is often expressed in Java with Spring. It is an illustration, not a complete program: services, validation annotations and error handling are left out. The same API could be written in any language.

Java
@RestController@RequestMapping("/v1")class PhotoController {    private final PhotoService photos;    PhotoController(PhotoService photos) {        this.photos = photos;    }    @PostMapping("/photos")    ResponseEntity<CreatePhotoResponse> create(            @RequestHeader("Idempotency-Key") String idempotencyKey,            @Valid @RequestBody CreatePhotoRequest request,            @AuthenticationPrincipal User caller) {        CreatePhotoResponse created = photos.create(caller.id(), request, idempotencyKey);        return ResponseEntity.created(URI.create("/v1/photos/" + created.photoId())).body(created);    }    @GetMapping("/feed")    FeedPage feed(@RequestParam(required = false) String cursor,                  @RequestParam(defaultValue = "20") int limit,                  @AuthenticationPrincipal User caller) {        int pageSize = Math.min(Math.max(limit, 1), 100); // cap the page size        return photos.feed(caller.id(), cursor, pageSize);    }    @PutMapping("/photos/{photoId}/likes/me")    ResponseEntity<Void> like(@PathVariable String photoId, @AuthenticationPrincipal User caller) {        photos.like(caller.id(), photoId); // idempotent: liking twice leaves one like        return ResponseEntity.noContent().build();    }}

The interesting design decisions are all visible without the rest of the code: the idempotency key on creation, the capped page size, the 201 with a Location, and the idempotent PUT for likes.

Trade-offs in API design

#

Decision

Trade-off

Fine-grained versus coarse endpoints

Fine-grained endpoints are reusable but chatty over long distances; coarse ones (a screen's data in one call) are fast but couple the API to one client

Offset versus cursor pagination

Offsets allow page jumps; cursors are stable and fast at depth

REST versus gRPC versus GraphQL

Simplicity and caching versus performance and contracts versus client flexibility

Strict versus lenient validation

Strict validation catches errors early but makes evolution harder if clients send extra fields; accept unknown fields, reject invalid known ones

Versioning in the URL versus headers

Visibility and easy routing versus cleaner URLs

Common mistakes and failure modes

#
  • Verbs in URLs (/createPhoto) instead of methods on resources.
  • 200 OK with an error message in the body. Clients, proxies and monitoring then think the call succeeded.
  • Non-idempotent creation without a key. Retries on flaky networks create duplicates, which is the most common real-world failure this article prevents.
  • Unbounded lists with no pagination or page-size cap.
  • Offset pagination on infinite feeds, producing duplicates and slow deep pages.
  • Breaking changes without a version, which fail silently on old mobile apps that cannot be forced to update.
  • Leaking internals in errors: stack traces, SQL, server names.
  • Checking authentication but not authorisation per object, so any signed-in user can read anyone's data by changing an id.

In an interview

#
  • List four to six endpoints that cover the functional requirements. Do not design twenty.
  • For each, state method, path, the key request fields and the success response.
  • Point out any endpoint that must be idempotent and how you make it so.
  • Mention pagination for lists, and authentication and rate limiting once for the whole API.
  • If the system has real-time needs, say which channel carries them (article 05).

Exercises

#

Exercise 1

Fix the API

#

Rewrite this API in a resource-oriented style, with correct methods and success codes:

Code
POST /getUser            body: {"id": 42}POST /createOrder        body: {...}POST /deleteOrder        body: {"orderId": 77}GET  /cancelOrder?id=77POST /addItemToCart      body: {"cartId": 5, "productId": 9, "qty": 2}

Exercise 2: Safe to retry?

#

For each request, say whether a client can safely retry it after a timeout, and if not, how you would make it safe:

  1. GET /orders/77
  2. PUT /users/42/email with body {"email": "a@example.com"}
  3. POST /orders with a cart
  4. POST /accounts/9/deposits with {"amount": 500}
  5. DELETE /photos/123

Exercise 3: Pagination choice

#

Choose offset or cursor pagination for each, and explain:

  1. An admin page listing 300 coupon codes, with page numbers at the bottom.
  2. A chat app's message history, scrolling back through years of messages.
  3. An export API that lets partners download all 40 million orders.

Exercise 4: Design the API

#

Design the API for a simple ticket-booking service: list events, view seats for an event, hold seats for 10 minutes, confirm a booking with payment, and cancel a booking. Give methods, paths, success codes and at least three error cases, and say which operations need idempotency.

Show solutionHide solution
Code
GET    /users/42                      -> 200 with the userPOST   /orders                        -> 201 Created, Location: /orders/{orderId}DELETE /orders/77                     -> 204 No Content (or keep the record and use cancellation)POST   /orders/77/cancellation        -> 201 or 200, or PATCH /orders/77 {"status": "cancelled"}PUT    /carts/5/items/9  {"qty": 2}   -> 200 or 204

Notes:

  • GET /cancelOrder was dangerous: GET must be safe, and crawlers, link previews or prefetching can trigger GET requests automatically. An action that changes state must not be a GET.
  • In most businesses an order is not deleted but cancelled, so the history remains. Modelling cancellation as a state change is usually better than DELETE.
  • Using PUT /carts/5/items/9 with the quantity makes "set quantity to 2" idempotent. A POST that "adds 2" would add 4 on a retry.

Exercise 2

Show solutionHide solution
  1. Safe. GET is safe and idempotent.
  2. Safe. PUT sets the email to a fixed value; doing it twice gives the same result.
  3. Not safe. A retry may create a second order. Make it safe with an Idempotency-Key header: the server stores the key with the created order id and returns the same order for repeated keys. A unique constraint on the key in the database guards against two concurrent retries.
  4. Not safe, and worse than 3 because money is involved: a retry could deposit twice. Use an idempotency key, recorded in the same database transaction as the deposit, so the deposit and the "key used" record either both happen or neither does (article 20).
  5. Safe in effect. The second DELETE finds nothing to delete. Return 204 or 404 on the repeat; either way the end state is the same. Many APIs return 204 for both to keep retries simple.

Exercise 3

Show solutionHide solution
  1. Offset. The list is small, rarely changes while being viewed, and users want page numbers. Deep-page cost is irrelevant at 300 rows.
  2. Cursor. The list is long, new messages arrive constantly, and users scroll continuously. A cursor on (conversation id, message sequence number) gives stable, fast pages at any depth.
  3. Cursor. Offsets would get slower and slower across 40 million rows, and orders created or updated during the export would cause duplicates or gaps. A cursor on (created_at, order_id), with a maximum page size, lets the partner resume after a failure from the last cursor they processed.

Exercise 4

Show solutionHide solution

Endpoint

Success

Errors

Idempotency

GET /v1/events?city=...&cursor=...

200 list

400 bad filter

Safe

GET /v1/events/{eventId}/seats

200 seat map with availability

404

Safe

POST /v1/events/{eventId}/holds body {"seat_ids": [...]}

201 with hold id and expiry time

409 a seat is already held or booked; 422 too many seats; 429

Needs an idempotency key, so a retry does not create a second hold

POST /v1/bookings body {"hold_id": "...", "payment_method": "..."}

201 booking confirmed (or 202 if payment confirmation is asynchronous)

410 Gone or 409 hold expired; 402 or 422 payment declined

Must be idempotent: same key returns the same booking and never charges twice

POST /v1/bookings/{bookingId}/cancellation

200 cancelled, refund initiated

409 already cancelled or event started; 403 not owner

Naturally idempotent if a second cancellation returns the current cancelled state

The difficult parts of this system are not the API but what sits behind holds and bookings: preventing two people from holding the same seat, and expiring holds reliably. Article 38 designs it.

Where to go next

#

Sources & further reading

  1. rfc-editor.org
  2. rfc-editor.org
  3. rfc-editor.org
  4. datatracker.ietf.org
  5. docs.stripe.com
  6. learn.microsoft.com
  • Guide · Technology

    Start Here: Your Roadmap from Zero to Job-Ready DSA

  • Guide · Technology

    Heaps and Priority Queues

  • Guide · Career

    A LinkedIn Profile That Gets Recruiter Messages