π Module 11: REST API Architecture & Systems Design for High-Scale Payments
Targeted for: Juspay Elimination Round, Technical Interviews, Hackathon Part B System Discussions, and High-Scale Backend Engineering.
Core Focus: Roy Fielding's Architectural Constraints, Richardson Maturity Model, HTTP Semantics & RFC Specifications, Idempotency Engineering, Optimistic Concurrency Control (ETags), Keyset vs Offset Pagination, API Versioning Strategies, Webhook Delivery Guarantees, Distributed Rate Limiting, and Juspay-Specific Payment Gateway Case Studies.
π Table of Contents
- Foundational REST Architecture & Fielding Constraints
- The Richardson Maturity Model (RMM)
- REST vs Alternative Architectural Paradigms
- Resource Modeling & URI Design Principles
- Exhaustive HTTP Method Semantics: Safety, Idempotency & RFC Specifications
- HTTP Status Codes in Payment Gateways: Precision & Nuance
- 6.1 2xx Success Semantics (200, 201 with Location, 202 Accepted, 204)
- 6.2 3xx Redirections: 301/302 vs 307/308 (Why Payment POST Fails on 302)
- 6.3 4xx Client Failures: 400 vs 422, 401 vs 403, 404 vs 410, 409, 412, 415, 429
- 6.4 5xx Upstream Bank Failures: 500 vs 502 vs 503 vs 504
- 6.5 Standardized Error Payloads: RFC 7807 (Problem Details)
- High-Scale Idempotency Implementation in REST APIs
- Optimistic Concurrency Control via HTTP Headers (
ETags) - Pagination for High-Volume Ledgers: Keyset (Cursor) vs Offset
- Distributed Rate Limiting & Throttling
- API Versioning Strategies in Production
- Webhook Architecture: Reliable Asynchronous REST Callbacks
- API Security, Authentication & Hardening
- Juspay-Specific Interview Scenarios & Verbatim Defense
1. Foundational REST Architecture & Fielding Constraints
1.1 What REST Is and What It Is Not
- What REST Is: REpresentational State Transfer. An architectural style formulated by Dr. Roy Fielding in his 2000 doctoral dissertation (Architectural Styles and the Design of Network-based Software Architectures).
- What REST Is Not:
- REST is NOT a protocol. It is not a standard like HTTP, TCP, or SOAP.
- REST is NOT tied exclusively to HTTP, though in 99.9% of software engineering implementations, HTTP is the underlying application protocol used.
- REST is NOT simply returning JSON from a URL. A server returning JSON from
POST /getUserDetailsis an RPC-over-HTTP API, not a RESTful API.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β WHAT IS A REPRESENTATION? β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ A Resource is a conceptual entity (e.g., Order #991, Customer #42). β
β β’ A Representation is the serialized state of that resource at a point in β
β time transferred across the wire (e.g., JSON payload, XML, Protocol Buff).β
β β’ The client never directly touches the database row; it manipulates the β
β Resource via Representations transferred over HTTP. β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ1.2 The 6 Architectural Constraints
Fielding proved that an architecture adhering to these 6 constraints achieves scalability, simplicity, modifiability, visibility, portability, and reliability:
ββββββββββββββββββββββββββββββββββββββββββββββββ
β ROY FIELDING'S 6 CONSTRAINTS β
ββββββββββββββββββββββββ¬ββββββββββββββββββββββββ
β
ββββββββββββββββββββ¬ββββββββββββββββΌββββββββββββββββ¬βββββββββββββββββββ
βΌ βΌ βΌ βΌ βΌ
1. Client-Server 2. Stateless 3. Cacheable 4. Layered System 5. Uniform
Separation Server Responses Proxies / CDNs Interface
(UI vs Storage) Zero Session Must Declare Transparent to (4 Core
State Cache-Control Client Sub-rules)
β
6. Code on Demand (Optional) β
Server sends executable code β
(e.g., JS SDK script) βΌ
βββββββββββββββββββββββββ
β β’ Resource Ident (URI)β
β β’ Representation Manipβ
β β’ Self-Descriptive Msgβ
β β’ HATEOAS (Hypermedia)β
βββββββββββββββββββββββββ-
Client-Server Separation:
- The user interface and user state concerns are separated from the data storage and business logic concerns.
- Impact: Frontends (React web apps, Android SDKs, iOS apps) can evolve independently from backend payment microservices.
-
Statelessness (Crucial Interview Topic):
- Definition: Every request from client to server must contain all the context, authentication credentials, and state parameters necessary for the server to understand and process the request.
- The server must never store client session context in server memory (e.g., no in-memory Java
HttpSessionpinned to a single server). - Payment Relevance: If Server A handles request 1 and dies, Server B must be able to handle request 2 immediately because all auth (
Bearer token) and resource identifiers are inside the request itself. This enables horizontal elasticity behind load balancers.
-
Cacheability:
- Every response must explicitly or implicitly label itself as cacheable or non-cacheable (
Cache-Control: no-store, no-cache,ETag,Expires). - Payment Relevance: Read requests (e.g., supported payment methods
GET /v1/merchants/{id}/payment-methods) are heavily cached at CDNs/Redis. Payment mutations (POST /v1/payments) must strictly sendCache-Control: no-store, privateto prevent intermediary proxies from caching sensitive transaction data.
- Every response must explicitly or implicitly label itself as cacheable or non-cacheable (
-
Layered System:
- A client cannot tell whether it is connected directly to the end application server or to an intermediary (API Gateway, Reverse Proxy, CDN, Hardware Load Balancer).
- Payment Relevance: Juspay places NGINX/Envoy API gateways in front of merchant traffic to handle TLS termination, DDoS mitigation, mTLS validation, and distributed rate limiting before requests touch payment processing engines.
-
Uniform Interface:
- The cornerstone constraint that differentiates REST from all other network paradigms. Elaborated in Section 1.3.
-
Code on Demand (Optional Constraint):
- Servers can temporarily extend or customize client functionality by transferring executable code (e.g., compiled WebAssembly, JavaScript snippets like Juspay's
hyper-sdk.jsor Stripe.js injected into merchant checkout pages).
- Servers can temporarily extend or customize client functionality by transferring executable code (e.g., compiled WebAssembly, JavaScript snippets like Juspay's
1.3 The Uniform Interface Sub-Constraints & HATEOAS
Fielding defined 4 sub-requirements for a Uniform Interface:
- Identification of Resources:
- Resources are conceptually identified using stable URIs (
https://api.juspay.in/v1/orders/ord_1024). The URI identifies the resource, not its internal database table name or representation format.
- Resources are conceptually identified using stable URIs (
- Manipulation of Resources through Representations:
- When a client holds a representation of a resource (including headers), it has enough information to modify or delete the resource on the server (given adequate permissions).
- Self-Descriptive Messages:
- Each message includes enough metadata for the receiver to know how to parse it.
Content-Type: application/json; charset=utf-8specifies how to deserialize the payload.Content-Encoding: gzipinforms the receiver how to decompress the body.
- HATEOAS (Hypermedia as the Engine of Application State):
- A client interacts with a network application entirely through hypermedia provided dynamically by the server.
- The client does not hardcode URL routes for future states; the server tells the client what states and transitions are legal right now via embedded hypermedia links.
/* Example of strict HATEOAS JSON Response */
{
"order_id": "ord_882910",
"amount": 499900,
"currency": "INR",
"status": "PENDING",
"_links": {
"self": { "href": "/v1/orders/ord_882910", "method": "GET" },
"pay": { "href": "/v1/orders/ord_882910/payments", "method": "POST" },
"cancel": { "href": "/v1/orders/ord_882910/cancel", "method": "POST" }
}
}If the order transitions to PAID, a subsequent GET will omit "pay" and "cancel" links and instead present a "refund" link.
2. The Richardson Maturity Model (RMM)
Leonard Richardson categorized web services into 4 progressive tiers of REST compliance:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Level 3: Hypermedia Controls (HATEOAS) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Level 2: HTTP Verbs & Explicit Status Codes β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Level 1: Individual Resource URIs (URI-based routing) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Level 0: The Swamp of POX (Single URI, RPC over HTTP/POST)β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ| Level | Description | Example Request | Characteristics |
|---|---|---|---|
| Level 0 | The Swamp of POX (Plain Old XML/JSON) | POST /api/service with body {"action": "createPayment", "data": {...}} | Single endpoint, single HTTP method (always POST). HTTP is treated merely as a transport tunnel (like SOAP or XML-RPC). Always returns 200 OK even on errors. |
| Level 1 | Resources | POST /orders, POST /customers/42 | Separate URIs for separate business entities. However, still uses a single verb (typically POST) for everything, or misuses GET for mutations. |
| Level 2 | HTTP Verbs & Status Codes | GET /orders/ord_1 $\rightarrow$ 200 OK<br>POST /orders $\rightarrow$ 201 Created<br>DELETE /tokens/tk_9 $\rightarrow$ 204 No Content | Leverages full HTTP method semantics (GET, POST, PUT, PATCH, DELETE) and standardized status codes (2xx, 4xx, 5xx). This is where 99% of industry "REST" APIs operate. |
| Level 3 | Hypermedia Controls (HATEOAS) | GET /orders/ord_1 returns JSON with _links (HAL, JSON-LD) | Responses dynamically guide the client through state transitions. Client hardcodes only the initial entry point. |
2.2 Why Real-World Payment Gateways Stop at Level 2
Interviewers at Juspay frequently ask: "Why don't Stripe, Razorpay, or Juspay implement Level 3 (HATEOAS)?"
- Client-Side Coupling to Strong Typed SDKs:
- Modern payment integration is done via strongly typed SDKs (Java, Python, Node, Go, Swift, Kotlin). Merchants don't dynamically parse links; they invoke SDK functions (
juspay.orders.create(...)).
- Modern payment integration is done via strongly typed SDKs (Java, Python, Node, Go, Swift, Kotlin). Merchants don't dynamically parse links; they invoke SDK functions (
- Payload Bloat & Mobile Latency:
- Adding
_linksmetadata to millions of micro-transactions inflates JSON payload size by 30-50%, increasing serialization overhead, network bandwidth, and packet fragmentation on poor 3G/4G networks in India.
- Adding
- UI Determinism:
- Frontend developers need to build static UI views (e.g., an iOS checkout button). If the UI depends on dynamic HATEOAS discovery, frontend logic becomes overly complex without tangible benefits.
3. REST vs Alternative Architectural Paradigms
3.1 REST vs GraphQL vs gRPC vs WebSockets
βββββββββββββββββ¬βββββββββββββββββββββββ¬βββββββββββββββββββββββ¬βββββββββββββββββββββββ¬βββββββββββββββββββββββ
β Metric β REST (HTTP/1.1 or 2) β GraphQL (HTTP) β gRPC (HTTP/2) β WebSockets (TCP) β
βββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββ€
β Serialization β JSON / Text β JSON / Text β Protobuf (Binary) β Binary or Text (JSON)β
βββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββ€
β Network Conn β Stateless Req/Resp β Stateless Req/Resp β Multiplexed Streams β Full-Duplex Persistentβ
βββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββ€
β Schema Model β OpenAPI / Swagger β GraphQL Schema (SDL) β Strict `.proto` file β Ad-hoc / Protocol-lessβ
βββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββ€
β Caching β Native HTTP/CDN β Difficult (Single URIβ Custom / Application β None (Stateful conn) β
β β (Cache-Control/ETag) β POST endpoint) β layer only β β
βββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββ€
β Performance β Moderate β Moderate to High CPU β Ultra-High (7-10x) β High (Low overhead) β
βββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββ€
β Best Fit In β Merchant Public APIs,β Complex aggregation β Internal low-latency β Live status updates, β
β Payments β Partner Webhooks β dashboards (Admin UI)β microservices, switchβ payment timer ticks β
βββββββββββββββββ΄βββββββββββββββββββββββ΄βββββββββββββββββββββββ΄βββββββββββββββββββββββ΄βββββββββββββββββββββββ3.2 The Juspay Microservices Protocol Split (North-South vs East-West)
[ Merchant Backend / Mobile App ]
β
β NORTH-SOUTH TRAFFIC:
β REST over HTTPS (JSON)
β (Idempotency, HMAC Signatures,
β OpenAPI, Merchant SDKs)
βΌ
ββββββββββββββββββββββββββββ
β Juspay API Edge Gateway β
β (NGINX / Envoy Proxy) β
βββββββββββββββ¬βββββββββββββ
β
ββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββ
β β β
β EAST-WEST TRAFFIC: β EAST-WEST TRAFFIC: β EAST-WEST TRAFFIC:
β gRPC over HTTP/2 (Protobuf) β gRPC over HTTP/2 (Protobuf) β Kafka Event Streams
β Ultra-low latency (<2ms) β Strong Binary Types β Async Ledger Posting
βΌ βΌ βΌ
βββββββββββββββββββββββ βββββββββββββββββββββββ βββββββββββββββββββββββ
β UPI Switch Router β β Fraud Engine / Riskβ β Double-Entry DB β
β (Haskell / Rust) β β Service β β (PostgreSQL) β
βββββββββββββββββββββββ βββββββββββββββββββββββ βββββββββββββββββββββββ- North-South (External $\rightarrow$ Gateway): REST over HTTPS is used because thousands of external merchant developers consume it across dozens of languages. Universal debugging (cURL, Postman), standard HTTP status codes, and HTTP-native caching are mandatory.
- East-West (Internal Microservices): gRPC over HTTP/2 is used internally because Protobuf serialization is drastically faster than JSON parsing, connections are multiplexed, and strict code-generated interfaces eliminate runtime schema mismatch bugs.
4. Resource Modeling & URI Design Principles
4.1 Plural Nouns, Hierarchies, and Path Depth
- Always Use Plural Nouns for Collections:
GET /v1/orders(List orders)POST /v1/orders(Create order)GET /v1/orders/ord_101(Retrieve single order)- Never use verbs:
GET /v1/getAllOrdersorPOST /v1/createPaymentare anti-patterns.
- Hierarchical Relationships (Parent $\rightarrow$ Child):
GET /v1/merchants/mer_99/transactions(All transactions under merchant 99)GET /v1/orders/ord_101/payments/pay_505(Specific payment attempt for order 101)
- The "Rule of Two Levels" (Preventing URI Explosion):
- When resource nesting exceeds 2 levels (e.g.,
/merchants/{m_id}/orders/{o_id}/payments/{p_id}/refunds/{r_id}), URI readability and routing become brittle. - Best Practice: Flatten child resources that have globally unique identifiers:
- Instead of:
GET /merchants/1/orders/2/payments/3/refunds/4 - Use:
GET /v1/refunds/rf_4(Refund ID is globally unique). - Filter via query parameter if querying a collection:
GET /v1/refunds?payment_id=pay_3.
- Instead of:
- When resource nesting exceeds 2 levels (e.g.,
4.2 Handling Non-CRUD Business Actions (Refund, Capture, Cancel, Verify)
In a pure CRUD world, you only create, read, update, or delete. But payment systems are driven by state machine transitions:
- Authorize $\rightarrow$ Capture
- Payment $\rightarrow$ Refund
- Order $\rightarrow$ Cancel
- VPA (UPI ID) $\rightarrow$ Verify
How do you model these in REST without violating Roy Fielding's principles?
βββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββ
β Pattern β Example URI β When to Use in Payment Gateways β
βββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ€
β Pattern A: Treat Action as a β `POST /v1/payments/pay_1/refunds`β Preferred when the action generates an β
β Sub-Resource (Recommended) β β independent entity that has its own ID, β
β β β lifecycle, and audit trail (Refunds). β
βββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ€
β Pattern B: Dedicated Action β `POST /v1/payments/pay_1/capture`β Used for state machine trigger transitionsβ
β Controller / Verb β `POST /v1/orders/ord_1/cancel` β where no separate sub-entity is spawned. β
β β β (Stripe, Razorpay, Juspay standard). β
βββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ€
β Pattern C: State Patching β `PATCH /v1/orders/ord_1` β Cleanest from purist REST theory, but β
β via Representation β Body: `{"status": "CANCELLED"}` β dangerous in payments because side- β
β β β effects (bank network calls) get masked. β
βββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββ5. Exhaustive HTTP Method Semantics: Safety, Idempotency & RFC Specifications
5.1 Safe vs Idempotent vs Non-Idempotent Matrix
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MATHEMATICAL AXIOMS β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Safe: Read-only. State(S) after Request = State(S) before. β
β Idempotent: f(f(x)) = f(x). Executing operation 1 time produces identical β
β server side-effects as executing it N times. β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ| HTTP Method | Safe? | Idempotent? | RFC | Typical Request Body? | Payment Gateway Semantics |
|---|---|---|---|---|---|
| GET | Yes | Yes | RFC 9110 Β§9.3.1 | Forbidden / Ignored | Fetch transaction details, check UPI payment status. |
| HEAD | Yes | Yes | RFC 9110 Β§9.3.2 | No | Fetch headers only (e.g., check ETag or Content-Length without body). |
| OPTIONS | Yes | Yes | RFC 9110 Β§9.3.7 | No | CORS Preflight requests (Access-Control-Allow-Methods). |
| POST | No | No* | RFC 9110 Β§9.3.3 | Yes | Create order, initiate payment. (*Made idempotent via Idempotency-Key). |
| PUT | No | Yes | RFC 9110 Β§9.3.4 | Yes | Complete replacement of resource. If re-run, end state is identical. |
| PATCH | No | No** | RFC 5789 | Yes | Partial update. (**Not strictly idempotent by spec). |
| DELETE | No | Yes | RFC 9110 Β§9.3.5 | Optional | Revoke stored payment token. Returns 204 or 404 on repeats. |
5.2 PUT vs PATCH: JSON Merge Patch (RFC 7386) vs JSON Patch (RFC 6902)
Common Juspay Interview Trap: "Is PATCH idempotent? What is the difference between PUT and PATCH?"
-
PUT Semantics (Full Replacement):
- Client sends the entire new representation.
- If a field is omitted from the request body, the server must overwrite that field with
nullor its default value! - PUT is strictly idempotent: replacing resource X with state Y multiple times results in state Y.
-
PATCH Semantics (Partial Delta Update):
- Client sends only the fields that need mutation.
- Why PATCH is NOT guaranteed to be idempotent:
- If the patch contains an operation like
"increment_attempts"or"append_to_log", executing it 3 times results in +3 increments.
- If the patch contains an operation like
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TWO RFC PATCH STANDARDS β
ββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββ
βΌ βΌ
RFC 7386: JSON Merge Patch RFC 6902: JSON Patch
Content-Type: application/merge-patch+json Content-Type: application/json-patch+json
Send partial JSON object: Send array of atomic operations:
{ [
"metadata": { "department": "fintech" }, { "op": "replace", "path": "/status", "value": "HOLD" },
"notes": null <-- Explicitly deletes field { "op": "add", "path": "/tags/0", "value": "vip" }
} ]5.3 Payloads in GET and DELETE: The RFC 9110 Reality
- GET with Body:
- RFC 9110 states: "Although request bodies on GET are allowed by syntax, they have no defined semantics and implementations SHOULD reject or ignore them."
- Production Reality: Intermediaries (AWS CloudFront, Akamai, Cloudflare, NGINX) frequently strip or drop request bodies on GET requests. Never design a REST API requiring a body in GET.
- DELETE with Body:
- RFC allows a body in DELETE requests. However, some HTTP client libraries (e.g., older Android OkHttp or Angular
$http) historically threw errors when sending payloads with DELETE. Use query parameters for DELETE wherever possible.
- RFC allows a body in DELETE requests. However, some HTTP client libraries (e.g., older Android OkHttp or Angular
6. HTTP Status Codes in Payment Gateways: Precision & Nuance
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β THE STATUS CODE FAMILY TREE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 2xx: Server understood and completed request successfully. β
β 3xx: Further action needed by user agent to complete request (Redirect). β
β 4xx: Client fault (Bad input, unauthenticated, unauthorized, race condition)β
β 5xx: Server fault (Bug in code, upstream bank timeout, database crash). β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ6.1 2xx Success Semantics
200 OK: Standard synchronous success response containing representation.201 Created: Resource successfully created. RFC Mandate: MUST return aLocationheader pointing to the newly minted resource.HTTP/1.1 201 Created Location: /v1/payments/pay_008129 Content-Type: application/json {"payment_id": "pay_008129", "status": "INITIALIZED"}202 Accepted: The Asynchronous Pattern. The request has been accepted for processing, but processing is incomplete. Crucial for asynchronous bank rails (e.g., initiating a UPI Mandate or NEFT payout). Returns a polling URL or status token.204 No Content: Action succeeded, but there is no entity body to return (e.g.,DELETE /v1/tokens/tok_123or acknowledging a webhook test ping).
6.2 3xx Redirections: 301/302 vs 307/308 (Why Payment POST Fails on 302)
Crucial Payment System Interview Question: "Why will an API gateway break payment processing if it redirects HTTP to HTTPS using status 302 or 301?"
βββββββββ¬βββββββββββββββββββββββ¬βββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Code β Name β Method Preservation β Browser / HTTP Client Behavior β
βββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β **301**β Moved Permanently β β **May Rewrite** β Spec originally disallowed, but browsers historicallly β
β β β POST $\rightarrow$ GETβ change POST to GET and drop request body! β
βββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β **302**β Found (Temporary) β β **May Rewrite** β Standard practice rewrites POST to GET, dropping body. β
βββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β **307**β Temporary Redirect β β
**Strict Preserve**β User agent MUST re-issue request with IDENTICAL METHOD β
β β β POST $\rightarrow$ POSTβ and IDENTICAL BODY to the new URI. β
βββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β **308**β Permanent Redirect β β
**Strict Preserve**β Permanent redirect that strictly preserves POST method β
β β β POST $\rightarrow$ POSTβ and payload. (RFC 7538). β
βββββββββ΄βββββββββββββββββββββββ΄βββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ[!CAUTION] If a merchant calls
http://api.juspay.in/v1/paymentsvia POST and the load balancer responds with301or302 Moved, the client library will redirect tohttps://...via GET, dropping the JSON payment body! The gateway must use308 Permanent Redirector force HTTPS at the DNS/HSTS layer.
6.3 4xx Client Failures
400 Bad Request: Generic client syntax failure (malformed JSON syntax, invalid JSON tokens).401 Unauthorized: Authentication is missing or invalid. The user has not established their identity. RFC 9110 requires the server to return aWWW-Authenticateheader specifying the challenge scheme.403 Forbidden: The client is authenticated, but does not possess permission to perform this action on this resource (e.g., Merchant A trying to fetch Merchant B's order, or using a sandbox API key against the production live cluster).404 Not Found: Resource URI does not exist.405 Method Not Allowed: E.g., sendingDELETE /v1/orders. RFC requires returning theAllowheader listing permitted methods (Allow: GET, POST).409 Conflict: Request conflicts with current state of the server.- Payment Use-Case 1: Calling
POST /v1/paymentswith anIdempotency-Keythat is currently in-flight by a parallel thread. - Payment Use-Case 2: Attempting to refund an order that is already in state
REFUNDED.
- Payment Use-Case 1: Calling
410 Gone: Resource previously existed, but has been deliberately deleted or expired permanently (e.g., an expired one-time payment link). Crawlers and clients must cease querying this URI.412 Precondition Failed: Used in conjunction withIf-MatchETags. Signals that the resource was modified by another actor before this request could be applied.415 Unsupported Media Type: Client sent a body withContent-Type: text/xmlwhen server expectsapplication/json.422 Unprocessable Entity(RFC 4918 / RFC 9110):- The JSON is syntactically valid (not a 400), but contains semantic business validation failures (e.g.,
"amount": -500,"currency": "XYZ","expiry_month": 15).
- The JSON is syntactically valid (not a 400), but contains semantic business validation failures (e.g.,
429 Too Many Requests: Client exceeded rate limits. Must includeRetry-After: <seconds>header.
6.4 5xx Upstream Bank Failures: 500 vs 502 vs 503 vs 504
When Juspay acts as a payment switch between a merchant and banking rails (HDFC, ICICI, NPCI), 5xx errors have strictly distinct meanings:
βββββββββββββββββββ REST Call βββββββββββββββββββ Socket / ISO 8583 βββββββββββββββββββ
β Merchant System β βββββββββββββββββββ> β Juspay Switch β βββββββββββββββββββββββ> β Upstream Bank β
β β <βββββββββββββββββββ β (Our API) β <βββββββββββββββββββββββ β (HDFC / NPCI) β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ500 Internal Server Error: An unhandled exception or crash inside Juspay's own application code (e.g., NullPointerException, unhandled Rust panic).502 Bad Gateway: Juspay's edge proxy (NGINX/Envoy) received an invalid, malformed, or TCP connection reset from our internal backend container or bank switch.503 Service Unavailable: Juspay's service is temporarily overloaded, under maintenance, or the Circuit Breaker to the bank has tripped OPEN due to high bank error rates.504 Gateway Timeout: Juspay sent the transaction request to the upstream bank (or payment network), but the bank did not return a response within the configured socket timeout (e.g., 15-30 seconds).- Critical Payment Semantics: A 504 NEVER means the payment failed! The customer's account may have been debited. The transaction status is
PENDINGuntil reconciled.
- Critical Payment Semantics: A 504 NEVER means the payment failed! The customer's account may have been debited. The transaction status is
6.5 Standardized Error Payloads: RFC 7807 (Problem Details)
Professional payment APIs do not return arbitrary error JSON. They follow RFC 7807 (Problem Details for HTTP APIs) (Content-Type: application/problem+json):
{
"type": "https://docs.juspay.in/errors/insufficient-funds",
"title": "Insufficient Account Balance",
"status": 422,
"detail": "Customer VPA user@oksbi has insufficient balance to complete βΉ4,999.00 transaction.",
"instance": "/v1/payments/pay_99812/attempts/att_01",
"code": "BANK_ERROR_INSUFFICIENT_FUNDS",
"decline_code": "DECLINED_BY_ISSUER",
"retryable": false
}7. High-Scale Idempotency Implementation in REST APIs
7.1 The Two-Generals & Network Timeout Dilemma
[ Merchant Client ] ββββββββ POST /v1/payments (βΉ10,000) ββββββββ> [ Juspay ]
βββ [ Bank Debits Customer Account ]
β
βΌ
[ Merchant Client ] <xxxxxx NETWORK CONNECTION DROPS (TIMEOUT) xxxxxx [ Juspay ]The merchant's client times out after 10 seconds. The merchant does not know:
- Did the request fail before reaching Juspay?
- Did Juspay fail before reaching the Bank?
- Did the Bank succeed, but the response packet was dropped on the wire?
If the merchant retries blindly, they risk double-debiting the customer.
7.2 Architecture of an Idempotency Filter (Redis Lock + DB Store)
POST /v1/payments
Header: "Idempotency-Key: 7b6c31a8-..."
β
βΌ
βββββββββββββββββββββββββ
β Idempotency Middlewareβ
βββββββββββββ¬ββββββββββββ
β
βββββββββββββββββ΄ββββββββββββββββ
β Compute SHA-256(Request Body) β
βββββββββββββββββ¬ββββββββββββββββ
β
βΌ
Query Redis / PostgreSQL: Idempotency Record
β
βββββββββββββββββββββββββΌββββββββββββββββββββββββ
βΌ βΌ βΌ
[ KEY EXISTS ] [ KEY EXISTS ] [ KEY NOT FOUND ]
Status: COMPLETED Status: IN_PROGRESS β
β β βΌ
β βΌ Acquire Distributed Lock
β Return 409 Conflict (Redis `SET NX EX 120`)
β "Concurrent request β
β with same key" βΌ
βΌ Insert DB Record:
Return Cached Response Status: `IN_PROGRESS`
Header: β
`Idempotent-Replayed: true` βΌ
Execute Payment Workflow
with Downstream Bank
β
βΌ
Update DB Record:
Status: `COMPLETED`
Store Status Code & Body
β
βΌ
Release Redis Lock
Return Response7.3 Payload Hash Mismatch Detection & Concurrency Edge Cases
Juspay Interview Question: "What if a merchant sends the same Idempotency-Key twice, but changes the amount from βΉ100 to βΉ500 in the second request?"
- Answer: The server must compute and store a SHA-256 digest of the request payload alongside the idempotency key.
- If
incoming_key == stored_keyANDincoming_payload_hash != stored_payload_hash:- The server must immediately reject the request with
422 Unprocessable Entityor400 Bad Requestwith error code:IDEMPOTENCY_KEY_PAYLOAD_MISMATCH. - Replaying a different payload with an old key indicates a catastrophic client-side programming error or malicious tampering attempt.
- The server must immediately reject the request with
8. Optimistic Concurrency Control via HTTP Headers (ETags)
8.1 The Lost Update Problem in Payment States
Merchant Agent A (Customer Care) Merchant Agent B (Disputes Desk)
β β
βββββ GET /v1/orders/1 (status: PENDING) ββββββββββββββββββ€
β β
β Agent A decides to CANCEL order β Agent B receives bank webhook:
β β order is PAID
β β
βββββ PATCH /v1/orders/1 (status: CANCELLED) β
β (Succeeds in DB!) β
β βββββ PATCH /v1/orders/1 (status: PAID)
β β (Overwrites Agent A! LOST UPDATE!)8.2 Strong vs Weak ETags (If-Match vs If-None-Match)
To eliminate the Lost Update problem without locking the database row during read operations:
- The Server generates an
ETagon read:HTTP/1.1 200 OK ETag: "v1_hash_9a8b7c6d" Content-Type: application/json {"order_id": "ord_1", "status": "PENDING", "version": 3} - The Client sends the conditional header on mutation:
PATCH /v1/orders/ord_1 HTTP/1.1 If-Match: "v1_hash_9a8b7c6d" Content-Type: application/json {"status": "CANCELLED"} - Server atomic validation:
UPDATE orders SET status = 'CANCELLED', version = version + 1 WHERE id = 'ord_1' AND version = 3;- If row count == 0, the record was modified concurrently. Server returns
412 Precondition Failed. - Agent B's request fails cleanly, prompting them to re-fetch the latest state.
9. Pagination for High-Volume Ledgers: Keyset (Cursor) vs Offset
9.1 Why Offset Pagination Degrades to $O(N)$ and Duplicates Records
Offset pagination (GET /v1/transactions?limit=20&offset=100000) relies on:
SELECT * FROM transactions ORDER BY created_at DESC LIMIT 20 OFFSET 100000;- Performance Catastrophe:
- The database engine must scan and traverse all $100,020$ index entries and discard the first $100,000$ in memory. Query latency degrades linearly $O(N)$ as page numbers increase.
- Data Inconsistency Under Concurrent Writes:
- In payment systems, thousands of transactions are inserted every minute.
- If a new transaction is inserted while the user is flipping from Page 1 to Page 2, all rows shift down by 1. Page 2 will return the last row of Page 1 again (Duplicate Record).
- If a transaction is deleted/purged, a record will be permanently skipped.
9.2 Keyset / Cursor Pagination Mechanics & DB Index Seek
Used by Juspay, Stripe, and modern high-throughput ledgers.
Request Page 1:
GET /v1/transactions?limit=2
Response:
{
"has_more": true,
"data": [
{"id": "tx_100", "created_at": 1726500000},
{"id": "tx_099", "created_at": 1726499990}
]
}
Request Page 2:
GET /v1/transactions?limit=2&starting_after=tx_099Database Query Generated:
-- Leverages B-Tree index seek O(log N) -> O(1) page access
SELECT * FROM transactions
WHERE id < 'tx_099'
ORDER BY id DESC
LIMIT 2;- Guarantees:
- $O(1)$ constant execution time regardless of whether fetching page 1 or page 10,000.
- Immune to concurrent inserts:
tx_099serves as an immutable navigational anchor.
10. Distributed Rate Limiting & Throttling
10.1 Rate Limiting Algorithms
βββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β Algorithm β Pros β Cons / Vulnerabilities β
βββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββ€
β **Token Bucket** β Handles bursts gracefully; β Requires atomic token computation; β
β (Standard in Gateways) β memory-efficient ($O(1)$ space). β complexity in distributed settings. β
βββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββ€
β **Leaky Bucket** β Smooths traffic out to constant β Bursts are delayed; buffers can fill β
β β egress rate. β and drop requests prematurely. β
βββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββ€
β **Fixed Window** β Trivial to implement β Boundary condition flaw: can receive β
β β (`INCR` with TTL in Redis). β $2\times$ the limit at window edge! β
βββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββ€
β **Sliding Window Log** β 100% accurate; no boundary flaw. β Memory explosion: stores timestamp per β
β β β request in a Redis Sorted Set ($O(N)$).β
βββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββ€
β **Sliding Window β Low memory footprint ($O(1)$); β Approximation (assumes uniform β
β Counter** (Cloudflare) β no edge-burst flaw. β distribution across previous window). β
βββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββ10.2 Standard Headers (Retry-After, X-RateLimit-*)
When throttling a client, payment APIs return:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1726500060
Retry-After: 45
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Please retry after 45 seconds."
}
}10.3 Distributed Redis Lua Atomic Rate Limiter
Why Lua is mandatory: Avoids race conditions (check-then-set) between distributed NGINX/API gateway nodes:
-- KEYS[1]: Rate limit key (e.g., ratelimit:merchant_123:POST:/v1/payments)
-- ARGV[1]: Max tokens allowed (e.g., 100)
-- ARGV[2]: Window size in seconds (e.g., 60)
local current = redis.call('INCR', KEYS[1])
if current == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[2])
end
if current > tonumber(ARGV[1]) then
return 0 -- RATE LIMITED
else
return 1 -- PERMITTED
end11. API Versioning Strategies in Production
11.1 The 4 Strategies Compared
βββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β Strategy β Implementation β Tradeoffs β
βββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ€
β 1. URI Path Versioning β `https://api.juspay.in/v1/orders` β Most common; explicit; trivially β
β (Industry Default) β `https://api.juspay.in/v2/orders` β routed at CDN/Load Balancer. β
β β β Violates purist REST (URI should β
β β β identify resource, not version). β
βββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ€
β 2. Custom Request Header β `X-API-Version: 2026-09-01` β Clean URIs. Supported by Stripe & β
β (Stripe / Juspay Style) β `Stripe-Version: 2024-06-20` β Juspay. Allows date-based granular β
β β β backward compatibility transforms. β
βββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ€
β 3. Content Negotiation (Accept) β `Accept: application/` β Purist Fielding REST approach. Hard β
β β `vnd.juspay.v2+json` β to test in browser/cURL; complex CDN β
β β β caching (`Vary: Accept` header). β
βββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ€
β 4. Query Parameter β `https://api.juspay.in/orders?v=2` β Easy for testing. Confuses caching β
β β β and feels like an ad-hoc hack. β
βββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββ11.2 The Stripe/Juspay Event Adapter & Backward Compatibility Pattern
How does a payment gateway support 100,000 merchants across 50 API versions without running 50 different microservices?
[ Incoming Request from Merchant on API Version 2020-08-01 ]
β
βΌ
βββββββββββββββββββββββββββββ
β Request Up-Transformationβ
β 2020 -> 2022 -> 2026 β
βββββββββββββββ¬ββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββ
β Core Business Logic β
β (Runs ALWAYS on LATEST) β
βββββββββββββββ¬ββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββ
β Response Down-Transform β
β 2026 -> 2022 -> 2020 β
βββββββββββββββ¬ββββββββββββββ
β
βΌ
[ Return 2020-08-01 Schema Response to Merchant ]- The internal microservices always execute on the latest schema.
- A pipeline of deterministic transformation gates translates older request formats forward, and translates newer response payloads backward to the merchant's pinned API version.
12. Webhook Architecture: Reliable Asynchronous REST Callbacks
12.1 Webhooks vs Long Polling vs Server-Sent Events
- Long Polling: Inefficient for 100,000 concurrent transactions; exhausts merchant and gateway socket descriptors.
- Server-Sent Events / WebSockets: Excellent for frontend checkout UI timer updates, but unsuited for server-to-server transactional settlement.
- Webhooks (Reverse REST): The payment gateway makes an HTTP
POSTrequest to the merchant's registered HTTPS URL whenever a background payment event occurs (order.successful,refund.failed).
12.2 Security: HMAC-SHA256 Signatures & Anti-Replay Timestamps
Because merchant webhook endpoints are exposed to the public Internet, attackers can forge fake payment.success webhooks to obtain free merchandise.
How Juspay Protects Webhooks:
POST /juspay/webhook HTTP/1.1
Host: merchant.com
Content-Type: application/json
X-Juspay-Signature: t=1726500120,v1=9b1c2d3e4f5a6b7c8d9e0f...Verification Algorithm:
- Merchant extracts timestamp $t$ (
1726500120) and signature $v1$. - Replay Attack Defense: Merchant checks $|t_{current} - t| \le 300\text{ seconds}$. If older than 5 minutes, reject immediately!
- Merchant computes: $$\text{Expected Signature} = \text{HMAC-SHA256}(\text{key} = \text{webhook_secret}, \text{data} = t + "." + \text{raw_payload})$$
- Constant-Time Comparison:
# Must use constant-time comparison to prevent Timing Attacks hmac.compare_digest(computed_signature, received_v1_signature)
12.3 Delivery Guarantees: Exponential Backoff, Jitter & Dead Letter Queues
Network failures are inevitable. If a merchant's server returns 500 or times out:
$$\text{Retry Delay} = \min(\text{MaxDelay}, \text{BaseDelay} \times 2^{\text{attempt}}) + \text{Full_Jitter}$$
Retry Schedule:
Attempt 1: Immediate
Attempt 2: 5s
Attempt 3: 15s
Attempt 4: 1m
Attempt 5: 15m
Attempt 6: 1h
Attempt 7: 6h -> Exhausted -> Route to Dead Letter Queue (DLQ) & alert merchant dashboard.- At-Least-Once Delivery: Merchants must design their webhook handlers to be idempotent (recording processed
event_idin a database with unique constraint).
13. API Security, Authentication & Hardening
13.1 Publishable vs Secret API Keys (PCI-DSS Scoping)
Payment platforms segregate credentials into two strict privilege tiers:
ββββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β Publishable API Key (`pk_live_...`) β Secret API Key (`sk_live_...`) β
ββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ€
β Safe to embed in Client Mobile Apps, β Kept strictly on Merchant's Backend. β
β React / HTML frontend code. β Never exposed to browser or mobile. β
β Scope: Tokenize card numbers, launch β Scope: Create charges, trigger β
β checkout UI, initiate UPI intent. β refunds, retrieve merchant bank info.β
ββββββββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββ13.2 Mutual TLS (mTLS) in Core Banking Interfacing
While public merchant APIs use one-way TLS (client verifies server certificate), communication between Juspay and Banking Switches (NPCI, Visa, HDFC) enforces mTLS (Mutual TLS):
- The Bank presents its certificate to Juspay.
- Juspay presents its cryptographically signed client certificate to the Bank.
- Both parties cryptographically authenticate each other at Layer 4 before any Layer 7 REST/HTTP request is transmitted.
14. Juspay-Specific Interview Scenarios & Verbatim Defense
Question 1: "Design the REST API for a high-volume UPI Collect Payment flow. Walk through request, response, and eventual settlement."
Verbatim Architecture Answer: "I would design this using an Asynchronous REST + Webhook Event Pattern:
- Initiation: The client issues:
POST /v1/paymentsHeaders:Idempotency-Key: <UUIDv4>,Authorization: Bearer <sk_key>Body:{"order_id": "ord_101", "vpa": "customer@okhdfcbank", "amount": 150000, "currency": "INR"}- Immediate Gateway Response: Because NPCI and the user's UPI PSP app take up to 60-90 seconds to authorize, the gateway cannot hold an open HTTP socket. The gateway writes an
IN_PROGRESSrecord, dispatches a message to the Kafka UPI Switch queue, and responds immediately with:HTTP/1.1 202 AcceptedBody:{"payment_id": "pay_001", "status": "PENDING", "poll_url": "/v1/payments/pay_001"}- Settlement Notification: Once the customer enters their UPI PIN and NPCI delivers the bank callback, Juspay dispatches an asynchronous Webhook to the merchant's endpoint:
POST https://merchant.com/webhookwithX-Juspay-Signature: t=...,v1=...Body:{"event": "payment.succeeded", "data": {"payment_id": "pay_001", "amount": 150000}}The merchant validates the HMAC-SHA256 signature, recordspayment_ididempotently, and responds with200 OK."
Question 2: "An upstream bank times out after 15 seconds. Should our REST API return 500, 502, 504, or 200 with status=PENDING? Why?"
Verbatim Architecture Answer: "Returning
500 Internal Server Erroris wrong because our server didn't crash. Returning502 Bad Gatewayis wrong because the upstream bank didn't send an invalid response; it sent no response at all.The correct architectural choice depends on whether the endpoint is synchronous or asynchronous:
- If an asynchronous payment resource model is used (recommended), the server should return
200 OK(or202 Accepted) with"status": "PENDING", because a payment timeout is not a terminal failureβthe money may still be debited from the user's account. Our background reconciliation cron or NPCI reversal job will determine the final state.- If the API contract strictly requires standard HTTP status reporting for a transport gateway failure, it must return
504 Gateway Timeout, accompanied by an explicit RFC 7807 payload stating"status": "PENDING", "retryable": false. The client must be instructed never to treat a 504 as a failed payment without running a status inquiry (GET /v1/payments/{id})."
Question 3: "Why is PUT idempotent, while PATCH is non-idempotent? Give a concrete code example."
Verbatim Architecture Answer: "PUT represents a complete replacement of the target resource. By definition of set equality: If I execute
PUT /v1/customers/1with payload{"name": "Aditi", "age": 28}, executing this 1 time or 100 times results in the exact same resource state:name = 'Aditi',age = 28.PATCH represents a partial delta modification. RFC 5789 does not guarantee idempotency because a patch payload can describe relative mutation operations rather than absolute values.
Concrete Example: Under RFC 6902 (JSON Patch), a client can issue:
[ { "op": "add", "path": "/failed_attempts", "value": 1 } ]Every time the server executes this PATCH request,
failed_attemptsis incremented by 1. Executing it 5 times results infailed_attempts = 5. Thus, $f(f(x)) \ne f(x)$, violating idempotency."
Question 4: "Explain how to implement distributed rate limiting without race conditions in a cluster of 50 API gateway nodes."
Verbatim Architecture Answer: "In a distributed cluster, a naive implementation of:
GET count- If
count < limitthenSET count = count + 1creates a classic Time-of-Check to Time-of-Use (TOCTOU) race condition where concurrent requests from multiple gateway nodes interleave, allowing traffic to blow past the rate limit.To solve this, we must execute the rate limiting check atomically in an in-memory datastore like Redis. We use a Redis Lua script or Redis
INCR+EXPIREpipeline. Because Redis is single-threaded in its command execution engine, the Lua script executes from start to finish without interleaving. If the incremented counter exceeds the threshold, the gateway rejects the request immediately with429 Too Many Requests, includingRetry-After: <seconds>andX-RateLimit-*headers, preventing the request from ever consuming backend worker threads or database connections."