Lessons
1 · System Design Foundations
2 · APIs and Communication
3 · Capacity Estimation
4 · Core Building Blocks
5 · Databases
- 12Relational Databases and Data Modeling for System Design
- 13Database Indexes and Query Performance: How to Make Reads Fast
- 14Transactions, Isolation Levels and Concurrency Control: Preventing Race Conditions
- 15Database Replication: Primary-Replica, Read Replicas, Lag and Failover
- 16Database Sharding and Partitioning: How It Works and When You Need It
- 17SQL vs NoSQL: Data Models and How to Choose the Right Database
6 · Distributed Systems
- 18Why Distributed Systems Are Hard: Partial Failure, Timeouts and Clocks
- 19CAP Theorem and Consistency Models Explained Without the Myths
- 20Idempotency, Retries and Message Delivery Guarantees
- 21Distributed Transactions: Two-Phase Commit, the Outbox Pattern and Sagas
- 22Distributed Locks, Leases, Leader Election and Consensus
7 · Reliability and Operations
8 · Architecture and Security
9 · Interview Method and Foundation Case Studies
10 · Real-World Case Studies
- 33Design a Notification System: Email, SMS and Push at Scale
- 34Design a Chat System Like a Messaging App
- 35Design a News Feed: Fan-Out, Ranking and the Celebrity Problem
- 36Design Search Autocomplete (Typeahead)
- 37Design a Distributed Job Scheduler
- 38Design a Ticket Booking System Without Overselling
- 39Design a Payment System: Idempotency, Ledgers and Reconciliation
11 · Large-Scale Case Studies
- 40Design a Video Streaming Platform: Upload, Transcoding and Adaptive Streaming
- 41Design a Ride-Hailing System: Location, Matching and Real-Time Updates
- 42Design a Food Delivery Platform: Orders, Restaurants and Delivery Partners
- 43Design an E-Commerce System: Catalog, Cart, Checkout, Inventory and Flash Sales
- 44Design a Web Search Engine: Crawling, Inverted Indexes and Ranking
12 · Interview Readiness
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.
For
Beginners and interview candidates who need to define clear, retry-safe APIs as part of a design, and developers who build REST services and want the system-design view.
Level
Before you start
Read How a Web Request Travels (article 03), especially the parts on HTTP methods, status codes and statelessness. Basic familiarity with JSON helps. The Java code is optional; read it as pseudocode if you use another language.
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:
- Model the API as resources (nouns): users, photos, comments, orders.
- Give each resource a URL:
/photos/123. - Use HTTP methods for the operation:
GETto read,POSTto create,PUTorPATCHto update,DELETEto remove. - Exchange representations, usually JSON, of the resource's state.
- Keep requests stateless: each request carries what the server needs, such as an authentication token.
URL design
#Good | Avoid | Why |
|---|---|---|
|
| The method already says "get" |
|
| Create by posting to the collection |
|
| Nested resources show the relationship |
|
| A like is a resource you create |
| 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? |
|---|---|---|---|
| Read a resource or list | Yes | Yes |
| Read headers only | Yes | Yes |
| Replace a resource at a known URL | No | Yes |
| Remove a resource | No | Yes |
| Create a resource, or trigger an action | No | No |
| 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.
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 |
|---|---|---|
| Success with a body | Reads, and updates that return the resource |
| A resource was created |
|
| Accepted for later processing | Long-running work handed to a background job (article 05) |
| Success, no body |
|
| Moved permanently | A URL has a new permanent home; clients and caches may remember it |
| Found / temporary redirect | Temporary redirects; the URL shortener in article 30 discusses which to use |
| Cached copy is still valid | Conditional |
| Malformed request | Invalid JSON, wrong types, missing required fields |
| Not authenticated | Missing or invalid credentials |
| Authenticated but not allowed | User may not access this resource |
| No such resource | Also used to avoid revealing that a private resource exists |
| Conflicts with current state | Username taken; booking a seat already booked |
| Conditional update failed |
|
| Well-formed but semantically invalid | Date range where the end is before the start |
| Rate limited | Include |
| Unexpected server failure | Bugs; never for client mistakes |
| An upstream failed or timed out | Usually produced by proxies and load balancers |
| Temporarily unable to serve | Overload or maintenance; may include |
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):
{ "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
#GET /users/42/photos?limit=20&offset=40The 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:
- 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.
- 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
#GET /users/42/photos?limit=20-> {"items": [...], "next_cursor": "aWQ6OA"}GET /users/42/photos?limit=20&cursor=aWQ6OAThe 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.
// 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))); }}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=aWQ6NQThe 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_urllets 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
Authorizationheader; 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 Requestsand, when you can, aRetry-Afterheader. 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-ControlandETaglet 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:
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 |
| Very visible and easy to route; the most common for public APIs |
Header |
| Keeps URLs clean; less visible |
Query parameter |
| 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 |
|---|---|---|---|
| Start an upload: send caption and metadata, receive an upload URL (article 10) |
|
|
| Read one photo's metadata |
|
|
| Delete own photo |
|
|
| Home feed, newest first |
|
|
| Like a photo |
|
|
| Unlike |
|
|
| Follow a user |
|
|
Notice two deliberate choices:
- Likes and follows use
PUTon a resource that represents "my like", which makes them naturally idempotent. Liking twice leaves you with one like, and retries are harmless. APOST /likesdesign 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.
@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 OKwith 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:
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:
GET /orders/77PUT /users/42/emailwith body{"email": "a@example.com"}POST /orderswith a cartPOST /accounts/9/depositswith{"amount": 500}DELETE /photos/123
Exercise 3: Pagination choice
#Choose offset or cursor pagination for each, and explain:
- An admin page listing 300 coupon codes, with page numbers at the bottom.
- A chat app's message history, scrolling back through years of messages.
- 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
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 204Notes:
GET /cancelOrderwas dangerous:GETmust 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/9with the quantity makes "set quantity to 2" idempotent. APOSTthat "adds 2" would add 4 on a retry.
Exercise 2
Show solutionHide solution
- Safe.
GETis safe and idempotent. - Safe.
PUTsets the email to a fixed value; doing it twice gives the same result. - Not safe. A retry may create a second order. Make it safe with an
Idempotency-Keyheader: 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. - 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).
- Safe in effect. The second
DELETEfinds nothing to delete. Return204or404on the repeat; either way the end state is the same. Many APIs return204for both to keep retries simple.
Exercise 3
Show solutionHide solution
- Offset. The list is small, rarely changes while being viewed, and users want page numbers. Deep-page cost is irrelevant at 300 rows.
- 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.
- 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 |
|---|---|---|---|
|
|
| Safe |
|
|
| Safe |
|
|
| Needs an idempotency key, so a retry does not create a second hold |
|
|
| Must be idempotent: same key returns the same booking and never charges twice |
|
|
| 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
#- Next: Synchronous vs Asynchronous Communication: Queues, Polling, WebSockets and Server-Sent Events decides how long requests and real-time updates should work.
- If APIs are new to you, What Is an API? A Plain-English Explanation With Real Examples starts from zero.
- Later: idempotency in depth (article 20), optimistic concurrency (article 14), API security (article 28) and rate limiting (article 31).
Sources & further reading
- Technology
- Software Engineering
- #APIs
- #backend
- #distributed systems
- #Java
- #roadmap
- #scalability
- #system design
- #system design interview
Connected lessons
Linked from 3 guides
Other pages in the library that point here.
Keep reading
More in Technology →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

