SwadhyAI Notes

All System Design notes

System Design APIs

REST API Design

You tap Pay $100 at checkout. The phone sends one HTTP request. A second later the network times out. Did the charge go through? If the app retries without care, the user might be billed twice.

That is why design matters. Good APIs name durable , use HTTP verbs correctly, and make retries, pagination, and versioning safe, not just pretty JSON.

What happens on Pay?

1

POST /charges identifies what you are creating.

2

Retries reuse the same key so you charge once.

3

Status + body

201 Created with a stable charge id, or a clear error.

4

Auth, quotas, and consistent errors keep the API safe.

Before naming endpoints, walk one request end to end so the later checklists have a concrete picture.

What actually happens in a REST call

A resource is the thing the API identifies; a representation is the information sent about it. The same order can have a JSON representation for an app and an HTML representation for a browser without becoming two different orders. An endpoint therefore needs more than a URL: define the accepted input, required permissions, returned fields, error cases, and what successful completion guarantees.

The client talks about a with a URL. /users/123 means "this user," not "run getUser." The HTTP method carries the intent: GET reads, POST creates, PUT replaces, PATCH patches, DELETE removes.

The server returns a representation: usually JSON, plus status codes caches and gateways already understand. That is why payment APIs, developer platforms, and cloud consoles are overwhelmingly REST-shaped: tooling, proxies, and browsers speak HTTP well.

At scale, the hard parts are rarely the first endpoint sketch. They are on writes, for huge lists, versioning without breaking mobile apps, and clear errors with request IDs.

With that mental model, decide when REST is enough and when another style earns its keep. The interview-style comparison lives in How to choose REST, gRPC, or GraphQL below; here is the short version.

How to choose REST (or not)

Prefer when clients need stable, cacheable URLs: public APIs, partner integrations, and CDN-friendly reads. GET plus a clear path is a natural cache key.

If web, iOS, and Android each over-fetch or chain many calls, may help. If traffic is internal and high-QPS, may help. Data model, caching, and fan-out usually matter more than protocol fashion: switch only when a concrete requirement appears.

Rule of thumb: start resource-oriented. Nest owned collections when that is the natural access path, then justify query flexibility (GraphQL), internal throughput or streaming (gRPC), or push events (SSE for one-way server push, WebSockets for bidirectional low-latency sync).

Design an endpoint in 8 steps

  1. 1. Name the resource with nouns: /orders/{orderId}/items.
  2. 2. Pick the method: GET read, POST create, PUT replace, PATCH partial update, DELETE remove.
  3. 3. Define the request body and validation errors.
  4. 4. Return a small response shape with stable IDs.
  5. 5. Choose status codes and one consistent error object.
  6. 6. Decide retry safety: idempotency key for non-safe writes.
  7. 7. Add pagination, filtering, field selection, and compression.
  8. 8. Add auth scopes, rate limits, request IDs, and versioning policy.

Once you keep REST, the next design fork is nouns versus verbs in the URL, then how nested ownership shows up in paths.

Resources vs operation-oriented APIs

Resource-oriented (REST default)

models the world as (nouns) you act on with HTTP verbs. Map domain entities to URLs (User → /users), nest owned collections (/users/{id}/posts), and let the method carry intent. This shape is the usual default for public and product APIs.

Operation-oriented (RPC-style)

  • Paths name actions like /getUser, so caching and docs stay harder.
  • Fits multi-step workflows that do not map cleanly to one resource (approve then notify).
  • Prefer a custom method on a resource: POST /orders/{id}:cancel.
  • is harder to reason about when every path invents its own verb.

Map entities → resources, then nest relationships

Start from the domain model. Each durable entity that clients address by ID becomes a resource collection. Child or owned collections become nested paths when the relationship is the primary access path.

Entity / relationshipResource path
User/users, /users/{id}
Posts owned by a user/users/{id}/posts
A specific post (global ID)/posts/{postId} (also fine)
Comments on a post/posts/{postId}/comments
Order line items/orders/{id}/items
http
# Collection + member
GET    /users              → list users
POST   /users              → create user
GET    /users/{id}         → read user
PUT    /users/{id}         → replace user (idempotent)
PATCH  /users/{id}         → partial update
DELETE /users/{id}         → delete user

# Nested relationship (user's posts)
GET    /users/{id}/posts   → list posts for user
POST   /users/{id}/posts   → create a post under that user
GET    /users/{id}/posts/{postId}  → read one nested post

# Deeper nest when ownership is clear
GET    /posts/{postId}/comments
POST   /posts/{postId}/comments
GET    /orders/{id}/items

Nest when the child is primarily accessed through the parent. Prefer a top-level resource (/posts/{id}) when clients address it by a stable global ID across contexts.

When to reach beyond REST

Teams already know HTTP, status codes, and . JSON is flexible to evolve (add optional fields) but pays a serialization and size cost versus binary formats. Start with REST unless the problem needs query flexibility, streaming, or extreme internal efficiency.

  • when many clients need different shapes and under/over-fetching dominates.
  • for high-QPS internal service calls with typed stubs and compact payloads.
  • SSE (server-sent events): one-way server-to-client stream over plain HTTP.
  • WebSockets: bidirectional low-latency sync for chat, collab, or game state.

Payload and bandwidth budget

Header overhead

Assume each request carries 2 KB of headers and the service receives 50,000 requests per second. Multiplying bytes per request by requests per second gives 100 MB/s of request headers alone, using decimal units. This excludes response bodies and lower-level overhead. Reducing a cookie helps repeatedly because the browser may attach it to every matching request.

Field trimming

8KB response trimmed to 800B saves 7.2KB. Across 1B reads, that is about 7.2TB less transfer.

Compression

100KB JSON compressed to 20KB saves 80KB. Across 10M responses, that is about 800GB less egress.

Fan-out

One 600B event to 10K subscribers becomes 6MB of outbound payload, before protocol and encryption overhead.

Endpoints alone are not enough. Clients also need safe retries, pagination, auth, evolution, and debuggable failures.

What a solid API design covers

Design checklist

  • Resource names first: nouns, stable IDs, and clear parent-child relationships.
  • Method semantics: safe GETs, idempotent PUT/DELETE, for POST retries.
  • Pagination and filtering: for feeds, bounded limits, deterministic ordering.
  • Contracts: , consistent error shape, request IDs, deprecation and sunset policy.
  • Protection: auth scopes, , quotas, validation, and backward-compatible versioning.

Debugging questions

  • Is the failure client validation, gateway rejection, auth, upstream timeout, or data conflict?
  • Can the client safely retry, or will duplicates create extra side effects?
  • Are p95 and p99 slow from payload size, N+1 calls, cache misses, or DB scans?
  • Do mobile or browser clients need smaller responses, compression, CORS, or fewer round trips?
  • Does the response include a stable error code and request ID support can trace?

How to choose REST, gRPC, or GraphQL

Choose the API style by client type, payload shape, caching needs, and operational maturity.

REST

Best default for browser and public HTTP APIs: cacheable resources, simple debugging, visible URLs, and broad infrastructure support.

gRPC

Best for controlled service-to-service calls: typed contracts, compact binary payloads, deadlines, streaming, and high request volume.

GraphQL

Best when many clients need different nested shapes: fewer round trips, less over-fetching, but harder caching and stricter query governance.

When debugging feels stuck

Split latency into client wait, gateway time, upstream fan-out, serialization, payload size, and cache behavior before changing protocols.

Simple rule of thumb

Start with REST for public clients, gRPC for busy internal RPC, and GraphQL only when nested shape flexibility is worth the caching and governance cost.

Protocol choice settles style. Maturity levels describe how fully you use HTTP's uniform interface, from RPC-over-HTTP up to hypermedia.

REST principles and maturity levels

Stateless

Each request carries all info needed. No server sessions, so any replica can serve any call.

Resource-based

URIs name (nouns): /users/123, not /getUser?id=123.

Uniform interface

HTTP methods (GET, POST, PUT, PATCH, DELETE) define the action with consistent conventions across endpoints.

Cacheable

Responses declare cacheability via headers (Cache-Control, ). At scale, caching can cut backend load sharply (see Numbers to know).

Richardson Maturity Model

L0

Single URI, single method (RPC-style): POST /api { "action": "getUser", "id": 123 }

L1

Multiple URIs (resources), still one method: POST /users/123 { "action": "get" }

L2

HTTP methods + status codes (most APIs here): GET /users/123 → 200 | POST /users → 201

L3

links in responses: "_links": { "posts": "/users/123/posts" }. Adds discoverability; grows payload size.

The checkout timeout in the hero is the classic write hazard. is how clients retry without double side effects.

Idempotency when retries are inevitable

Suppose a client creates an order with an idempotency key and loses the response. The server must associate that key with the caller, the original request, and its recorded result. A retry with the same key and equivalent input returns that result; reusing the key for a different order should be rejected. The key record and order creation need an atomic relationship so concurrent retries cannot both create an order. Define how long keys remain valid and what a client should do after that window expires.

MethodIdempotentSafeBodyNotes
GETYESYESNoAlways cacheable
POSTNONOYesUse idempotency keys
PUTYESNOYesFull resource replace
PATCHNO*NOYesPartial update
DELETEYESNONo2nd delete = 404 (OK)

Solution: Idempotency-Key pattern

http
POST /v1/charges
Headers: Idempotency-Key: "key_abc123"
Body: { "amount": 10000, "currency": "usd" }

Server:
1. Check Redis for "key_abc123"
   YES → return cached response (no re-execution)
   NO → process charge → store key + response (TTL: 24h)

Lists grow under live traffic. Offsets are simple but drift under inserts; keeps stable iteration.

Pagination: cursor vs offset

A cursor represents a position in a defined ordering, such as creation time followed by a unique identifier to break ties. After returning the first 20 records, the next request asks for records after the last pair it saw. This avoids scanning and discarding a large offset, but does not automatically provide a snapshot: concurrent inserts, deletions, or sort-key changes can still affect the traversal. State whether the API promises a live view or a stable export.

Offset pagination

http
GET /posts?offset=10000&limit=20
SQL: SELECT * FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 10000

The DB scans and discards 10,000 rows. Page 500 is much slower than page 1, and inserts between pages cause skips or duplicates.

Cursor pagination

http
GET /posts?after=post_abc&limit=20
SQL: SELECT * FROM posts WHERE id < 'post_abc' ORDER BY id DESC LIMIT 20

Time stays roughly constant per page, items are not skipped or duplicated, and the pattern scales to huge catalogs and feeds.

A few capacity figures show up across rate limits, caching, field masks, and auth. Keep them here and cross-ref instead of restating.

Numbers to know

  • Published API quotas vary: Maps ~50K req/day; video platforms ~10K units/day; payment APIs ~100 req/sec.
  • Developer APIs often sit near 5K req/hr; social read workloads near 900 reads per 15 minutes.
  • Cache-Control and ETag discipline can cut origin load by 90%+ on read-heavy public GETs.
  • Field masks: 10 KB → 2 KB (−80%) at 1B reads/day saves about 8 TB of egress per day.
  • JWT validation must stay sub-millisecond: short access tokens (~15 min) with longer refresh (~30 days).

Payload trimming math also appears in Payload and bandwidth budget above; field-mask walkthrough is in the wrap-up below.

With lists and writes under control, protect the edge and evolve the contract without bricking mobile clients overnight.

Rate limits, versioning, auth, and gateways

without mystery

Throttle abusive or bursty callers and return clear headers so clients know the limit, remaining budget, and when to retry. Example quotas live in Numbers to know.

http
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
Retry-After: 30
AlgorithmDescriptionUsed by
Token bucketTokens refill at a fixed rate; allows short bursts.Cloud platforms, payment APIs
Sliding windowCount requests in a rolling window; more precise.Developer APIs
Fixed windowCount per calendar interval; simple but ~2× burst at boundaries.Simple APIs

API versioning strategies

Plan how URLs, headers, or content negotiation evolve so pinned mobile clients do not brick overnight.

URL path

http
/api/v1/users
/api/v2/users

Cache-friendly and easy to understand; URLs accumulate versions. Common in payment and social APIs.

Header-based

http
Accept: application/vnd.api.v2+json

Clean URLs; harder to test from a browser. Common in developer APIs.

Query parameter

http
/users?version=2

Easy to switch; pollutes the query string and complicates cache keys. Some internal APIs.

Backward compatibility: never remove a field or change its type; add new fields as optional; deprecate with a sunset header. Payment APIs often keep clients pinned to the signup-time version for years.

Authentication patterns

Sessions, tokens, OAuth scopes, and gateway enforcement decide who can touch which resources without trusting clients blindly.

API keys

http
X-API-Key: sk_live_abc123...

Simple and easy to revoke; no end-user identity; server-to-server only. Common for payment, email, and messaging APIs.

OAuth 2.0 + JWT

http
Authorization: Bearer eyJhbG...

OAuth 2.0 delegates access; JWT (JSON Web Token) carries claims without a session store. Scoped and stateless; revocation is harder. Common for large public APIs.

mTLS (internal)

Mutual TLS: both client and server present certificates. Strongest zero-trust option; certificate management overhead; not for public browsers. Common in service meshes.

At billion-user scale, cache JWKS (JSON Web Key Set) at the edge and rotate signing keys without downtime. Timing targets are in Numbers to know.

Bulk operations and the API gateway

Batch endpoints and gateways aggregate traffic so microservices do not drown in chatty one-row calls.

Bulk operations (N calls → 1)

json
PATCH /users/bulk
{ "operations": [
  { "id": 1, "status": "active" },
  { "id": 2, "status": "active" }
] }

Response: { "succeeded": 987, "failed": 13 }

API gateway pattern

Client → API gateway → microservices. The gateway typically owns:

  • Authentication (JWT validation) so services trust a verified identity.
  • per user, IP, or key to protect upstreams.
  • Request routing (/users → User Service) and GET response caching.
  • Circuit breaking (fail fast if a service is down) and API version routing.

Contracts and conditional headers finish the public surface: machines share one schema, and writers do not clobber each other.

Contracts, errors, and optimistic concurrency

An error response should tell a client what it can safely do next. Validation errors require changing input; authentication failures require establishing credentials; conflicts require refreshing state; transient failures may permit bounded retries. Keep machine-readable error identifiers stable even when human-readable wording improves. For asynchronous work, return an accepted status and a status-resource identifier instead of implying that the final operation has already completed.

contracts and consistent errors

Machine-readable specs plus a stable error shape let humans and tooling integrate without guessing.

OpenAPI spec

yaml
openapi: 3.0.0
paths:
  /users:
    get:
      summary: List users
      parameters:
        - name: after
          in: query
        - name: limit (default: 20)

Auto-generates docs, SDKs, and mock servers.

Consistent errors

json
HTTP/1.1 402 Payment Required
{
  "error": {
    "type": "card_error",
    "code": "card_declined",
    "message": "Card declined.",
    "request_id": "req_abc"
  }
}

Every error: type, code, message, param, doc_url, request_id.

RFC 9457 problem details and OpenAPI 3.1

Prefer application/problem+json with stable fields: type, title, status, detail, instance (RFC 9457). OpenAPI 3.1 aligns JSON Schema 2020-12, supports webhooks, and documents problem responses per status (OpenAPI 3.1).

json
HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/version-conflict",
  "title": "Resource version conflict",
  "status": 409,
  "detail": "Profile was updated by another client.",
  "instance": "/users/123"
}
yaml
openapi: 3.1.0
info:
  title: Users API
  version: 1.0.0
paths:
  /users/{id}:
    get:
      responses:
        '200':
          description: OK
        '404':
          description: Not found
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetail'

Conditional requests and optimistic concurrency

Two users can read version 7 and then save different edits. If both writes replace the same record unconditionally, the later write silently erases the earlier one. A conditional update makes the version check and update one atomic storage operation. The first update creates version 8; the second fails its version-7 condition and must refresh or merge. Reading a version and checking it only in application memory leaves the race unresolved.

and If-* headers let caches and clients avoid wasted bandwidth and detect lost updates before they corrupt state.

HeaderUseTypical response
If-None-MatchCache revalidation (GET)304 Not Modified if ETag matches
If-MatchOptimistic concurrency (PUT/PATCH/DELETE)412 Precondition Failed if resource changed
If-Unmodified-SinceTime-based guard on writes412 if modified after timestamp

If-Match flow: GET /users/123 returns ETag: "v7" → client sends PATCH /users/123 with If-Match: "v7". Server applies the patch only if the version still matches; otherwise 412 and the client refreshes. Defined in RFC 9110 conditional requests.

Resource-oriented design at search scale

Patterns that keep pagination, caching headers, and graceful degradation workable at enormous request rates.

  • Resource-oriented: everything has a name, e.g. //storage.googleapis.com/buckets/my-bucket/objects/cat.jpg.
  • Standard methods first (List, Get, Create, Update, Delete); custom methods only when those do not fit.
  • Long-running ops: POST … → Operation, then poll GET /operations/op-456.
  • Field masks save bandwidth (?fields=name,email); savings in Numbers to know.
  • -based concurrency: If-Match on writes, same rule as above.

Close the lesson by locking the invariant, walking one capacity example and one failure timeline, then practicing and remembering the spine.

Correctness invariant

What must never become false

Unsafe writes with the same idempotency key produce at most one successful side effect, and conditional updates with / If-Match never silently overwrite a newer version (412 or equivalent, then client refresh). Resource URLs stay stable identities; versioning must not invent a second source of truth for the same entity.

Worked example: field masks at 1B requests/day

Average response 10 KB trimmed to 2 KB via field masks (−80%). Saving 8 KB × 1B reads/day ≈ 8 TB less egress per day (same figure as Numbers to know). Same resource; smaller projections. Pair with so deep offset scans do not multiply DB work.

Failure timeline: lost update without If-Match

T0  Client A GET /users/123 → ETag &quot;v7&quot;, display name Alice
T1  Client B GET /users/123 → ETag &quot;v7&quot;, edits email, PUT succeeds → &quot;v8&quot;
T2  Client A PUT name=Alicia without If-Match → overwrites email from B
T3  User-visible invariant broken: B&apos;s write vanished (lost update)
T4  Require If-Match: &quot;v7&quot; → A gets 412, refreshes, merges, retries

Optimistic concurrency is not optional once two clients can edit the same resource.

Exercises

Exercise 1

Design POST /charges with idempotency keys. What do you return on a replay after success, after failure, and while the first attempt is still in flight?

Exercise 2

Offset page=50000 of a 10M-row table is slow. Switch to cursor pagination. What breaks for "jump to page 50" UIs, and how do you explain it?

Exercise 3

Pick URI versioning vs header versioning for a public payment API. Name one migration that each makes harder.

Try it: pick the REST status code

Each card is a concrete API call. Select the status you would document for clients, then move through scenarios to build intuition for safe, predictable HTTP APIs.

GET /orders/42
(no body)
Attempts
0
Correct picks
0
Documented answer
200

Choose the HTTP status you would return, then check your answer against REST semantics.

Topic-specific references

What to remember

at scale is about consistency and developer experience. Prefer over offset, use for unsafe writes, version from day one, with clear headers, and ship error shapes that help people debug. Study strong public payment and resource-oriented APIs as living reference implementations.

Practice and further reading

Use these exercises to connect the lesson to a design decision. State your assumptions, trace what happens to one request, and explain the rule that must remain true even during a failure. That rule is the design's correctness invariant.

Focus for this lesson: Design stable resource contracts with correct methods, status codes, concurrency controls, idempotency, pagination, errors, and evolution.

Explain the decision

How do ETags prevent a lost update?

Read the reasoning

The client sends If-Match with the version it read; the server updates only if that version is still current, otherwise returns a precondition failure.

Apply it to a production scenario

Mobile clients retry POST /payments after a timeout. What contract is safe?

Read one approach

Require a scoped idempotency key, fingerprint the request, persist the first terminal response, and return the same result for matching retries.

Work through the numbers

A 1 MiB list response is requested 20,000 times/minute and 70% can return 304. Estimate payload bandwidth saved.

Read the worked calculation

20,000 × 0.70 MiB/min ≈ 14,000 MiB/min, about 233 MiB/s, excluding headers and compression.

Reason through a failure

What must cursor pagination guarantee during concurrent inserts?

Read the recovery reasoning

A stable ordering key and cursor boundary must prevent duplicates or skips according to the documented snapshot/freshness contract.