REST API Design: First Principles & Enterprise Architecture
1. Architectural Foundations of REST
Representational State Transfer (REST) is an architectural style for distributed hypermedia systems, formalized by Roy Fielding in his 2000 doctoral dissertation. Unlike prescriptive protocols such as SOAP or XML-RPC, REST defines a cohesive set of architectural constraints designed to maximize scalability, simplicity, modifiability, and performance across network boundaries.
When an architecture complies with these foundational constraints, it is considered RESTful.
graph TD
Client[Client Architecture] <-->|Hypermedia / HTTP| Gateway[API Gateway / Edge Layer]
Gateway <-->|State Transitions| Auth[Authentication Service]
Gateway <-->|Stateless Cache-Control| Cache[Distributed Cache Layer]
Gateway <-->|Resource Representations| Backend[Resource Servers / Microservices]
Backend --> Data[(Persistent Storage)]
Fielding's Six Architectural Constraints
- Client-Server Separation: The separation of concerns between user interface systems (clients) and data storage/processing systems (servers). This permits clients and servers to evolve independently, enabling frontend portability across web, mobile, and embedded platforms.
- Statelessness: Each request sent from client to server must contain all necessary information to understand and process the request. The server must not store any conversational session state between requests. Session state rests entirely on the client, enhancing horizontal scaling and fault tolerance.
- Cacheability: Responses must implicitly or explicitly declare themselves as cacheable or non-cacheable. When valid cache constraints are present, client and intermediary cache layers (CDNs, edge servers) reuse payload responses, significantly mitigating backend server load and network latency.
- Uniform Interface: The pivotal hallmark of REST. It decouples the conceptual service architecture from its concrete implementation, enforced through four sub-constraints:
- Identification of Resources: Resources are distinct from their structural representation (e.g., URI targeting
/api/v1/orders/9942). - Manipulation of Resources Through Representations: Clients holding a resource representation possess full capability to modify or delete the underlying state, provided authentication authorizes it.
- Self-Descriptive Messages: Packaged headers and media type definitions (
Content-Type: application/json) specify exact execution context for parser engines. - Hypermedia As The Engine of Application State (HATEOAS): Resource responses provide interactive links dictating allowable subsequent application state transitions.
- Identification of Resources: Resources are distinct from their structural representation (e.g., URI targeting
- Layered System: Constrains component observability such that individual nodes cannot ascertain whether they are communicating with an end server or an intermediary proxy, CDN, or load balancer.
- Code on Demand (Optional): Servers may temporarily transfer executable code (such as JavaScript client bundles) to extend client functionality.
2. Resource Modeling & URI Hierarchy
In REST, a resource represents any conceptual operational object—a user, an order, a financial debit, or a generated report. Resources must be represented via clear, intuitive, and deterministic Uniform Resource Identifiers (URIs).
Best Practices for URI Construction
- Use Noun-Based Plural Identifiers: Resources represent domain objects, not Remote Procedure Calls (RPCs). Never embed actions inside paths.
- Anti-Pattern:
POST /createAccountorGET /getAllUsers - RESTful Canonical:
POST /v1/accountsorGET /v1/users
- Anti-Pattern:
- Hierarchical Resource Nesting: Express clear parent-child structural relationships, but avoid exceeding three levels of nesting to prevent conceptual complexity and URI fragility.
- Valid:
GET /v1/users/usr_8492/orders/ord_1102 - Excessive:
GET /v1/users/usr_8492/orders/ord_1102/items/itm_33/shipment/status(Better:GET /v1/order-items/itm_33/shipment)
- Valid:
- Use Kebab-Case for Path Segments: Avoid CamelCase or snake_case within path segments to preserve consistency across case-sensitive file systems and gateway rules:
/v1/user-profiles/usr_99/account-settings.
3. HTTP Methods & Idempotency Mathematics
REST harnesses HTTP verbs to execute distinct domain state transitions. An engineering grasp of safety and idempotency is vital for resilient system design, client-side retries, and network fault tolerance.
Formal Definitions
- Safe Operations: Operations that perform read-only semantics without executing state alterations on the resource server ().
- Idempotent Operations: Operations where sequential identical invocations result in the exact same server state as a single execution. Formally expressed as a mathematical function: Where represents the server-side state transition algorithm applied to resource state .
| HTTP Method | RFC Semantic Purpose | Safe? | Idempotent? | Cacheable? | Payload Body |
| :--- | :--- | :--- | :--- | :--- | :--- |
| GET | Retrieve resource presentation | Yes | Yes | Yes | No |
| POST | Create subordinate resource / trigger processor | No | No | Only with Expires/Cache-Control | Yes |
| PUT | Full resource replacement or creation at known URI | No | Yes | No | Yes |
| PATCH | Partial resource mutation (RFC 5789) | No | No (Unless explicitly constructed) | No | Yes |
| DELETE | Annihilate specific resource | No | Yes | No | Optional |
| HEAD | Retrieve HTTP headers identical to GET without payload | Yes | Yes | Yes | No |
| OPTIONS | Probe CORS capabilities and supported methods | Yes | Yes | No | No |
Achieving Idempotency in POST and PATCH Operations
Because POST is non-idempotent by definition (calling POST /v1/orders three times creates three independent orders), high-reliability architectures implement Idempotency Keys.
The client inserts a unique UUIDv4 inside the request headers (Idempotency-Key: e8a91-4c11...). The API gateway or backend cache checks if this operation token has previously succeeded:
sequenceDiagram
autonumber
Client->>+API Gateway: POST /v1/charges (Idempotency-Key: K-881)
API Gateway->>+Distributed Cache (Redis): SETNX K-881 "IN_PROGRESS" TTL: 86400
alt Key Already Exists (Duplicate Request / Retry)
Distributed Cache-->>API Gateway: Key Exists (Returns previous saved cached payload)
API Gateway-->>Client: 200 OK (Returns exact initial HTTP response)
else Key Stored Successfully
Distributed Cache-->>-API Gateway: Success
API Gateway->>+Payment Service: Process Debit (Payload)
Payment Service-->>-API Gateway: 201 Created (Transaction ID: Tx-101)
API Gateway->>Distributed Cache: SET K-881 "Tx-101 Response Payload"
API Gateway-->>-Client: 201 Created (Transaction ID: Tx-101)
end
4. Comprehensive HTTP Status Code Taxonomy
API endpoints must return accurate semantic status codes to instruct automated clients, load balancers, and monitoring agents on how to interpret responses without manually parsing body error strings.
+-------------------------------------------------+
| HTTP Status Code Taxonomy Map |
+-------------------------------------------------+
| 2xx (Success) | 3xx (Redirection) |
| ├─ 200 OK | ├─ 301 Moved Perm. |
| ├─ 201 Created | ├─ 304 Not Modified |
| ├─ 202 Accepted | └─ 308 Perm. Redirect |
| └─ 204 No Content | |
+---------------------+---------------------------+
| 4xx (Client Error) | 5xx (Server Error) |
| ├─ 400 Bad Request | ├─ 500 Internal Error |
| ├─ 401 Unauth | ├─ 502 Bad Gateway |
| ├─ 403 Forbidden | ├─ 503 Service Unavail. |
| ├─ 404 Not Found | └─ 504 Gateway Timeout |
| ├─ 409 Conflict | |
| ├─ 422 Unprocess. | |
| └─ 429 Too Many Req| |
+-------------------------------------------------+
2xx Success
200 OK: Standard response for successful operations returning a data payload (GET, PATCH, PUT).201 Created: Returned when aPOSTorPUToperation successfully spawns a new record. Crucial Rule: Must include aLocationresponse header directing the client to the canonical URI of the generated entity (Location: /v1/orders/ord_5521).202 Accepted: Asynchronous processing handoff. Confirms payload syntax is valid and processing has been queued to an background worker pool, but completion is pending. Often returns an endpoint to poll status (/v1/jobs/job_991/status).204 No Content: Operation succeeded with a void body payload. The default architectural response for successfulDELETEexecutions.
3xx Redirection
301 Moved Permanently: The resource has permanently relocated to a new URI specified in theLocationheader. Clients and caching proxies update hardcoded bookmarks.304 Not Modified: Conditional read validation (usingIf-None-Match: "etag_value"orIf-Modified-Since). Instructs client to execute application logic utilizing its locally cached version without network payload transmission.308 Permanent Redirect: Unlike legacy301(where browsers and clients famously mutated POST requests into GETs upon redirection),308mandates that the redirect MUST preserve both the HTTP verb and the body payload.
4xx Client Error
400 Bad Request: Malformed request syntax, invalid JSON schema, or untyped query structures.401 Unauthorized: Authentication is entirely missing, corrupted, or cryptographically invalid (e.g., expired JWT). Semantic distinction: User identity is unknown.403 Forbidden: Identity is authenticated and verified, but existing IAM authorization policies reject operational access to the targeted resource.404 Not Found: Resource does not exist at the requested canonical path.409 Conflict: Request violates database domain invariants or concurrent operational rules (e.g., executing a user creation POST where the email address uniquely matches an existing tenant, or failing an optimistic concurrency lock via matchingETag).422 Unprocessable Entity: Payload syntactically valid JSON, but violates complex semantic validation constraints (e.g., passing a birth date located in the future, or requesting a debit amount greater than balance limit).429 Too Many Requests: Client breached allotted concurrency or volumetric rate-limiting thresholds.
5xx Server Error
500 Internal Server Error: Generic terminal fallback indicating uncaught exceptions or runtime faults.502 Bad Gateway: Upstream edge proxy (NGINX, Envoy) received an invalid protocol response or TCP reset from the upstream service architecture.503 Service Unavailable: Server explicitly unable to accept requests due to proactive graceful shedding, overloaded capacity queues, or scheduled database failovers. Often accompanied byRetry-After: 30header.504 Gateway Timeout: Edge gateway terminated the TCP execution socket because downstream workers or persistent storage engines failed to respond within strict network SLAs.
5. Pagination Architectures: Offset vs. Cursor
When REST endpoints expose unbounded domain sequences (GET /v1/logs), transmitting entire datasets risks memory exhaustion, network serialization latency, and database CPU locks. Effective pagination architecture defends server resource limits while providing seamless data consumption.
Offset-Based Pagination
Clients pass numeric page jumps via query parameters: GET /v1/events?limit=50&offset=2000.
- SQL Translation:
SELECT * FROM events ORDER BY created_at DESC LIMIT 50 OFFSET 2000; - Mathematical Complexity: where is the limit and is the offset. The database engine must read, process, and discard the first 2,000 records from index blocks before delivering the target 50 rows. As offsets increase toward millions of rows, execution latency degraded exponentially.
- Anomalies: Vulnerable to data drift. If a record is inserted or deleted while a user navigates between pages, items slip across boundaries, causing skipped records or duplicates.
Cursor-Based (Relay / Keyset) Pagination
Instead of positional numeric jumping, cursor pagination relies on an immutable, globally ordered index anchor (typically a composite key of timestamp + unique_id).
graph LR
subgraph Client Request
C1["GET /v1/events?limit=2&after=eyJpZCI6NzgxLCJ0cyI6IjIwMjYtMDgtMDFUMDA6MDA6MDAuMDAwWiJ9"]
end
subgraph API & DB Layer
D1[Decode Base64 Cursor] --> D2["Extract: timestamp = '2026-08-01', id = 781"]
D2 --> D3["SQL: WHERE (created_at, id) < ('2026-08-01', 781) ORDER BY created_at DESC, id DESC LIMIT 3"]
D3 --> D4[Return 2 Items + Next Cursor from 3rd Item]
end
- Mathematical Complexity: or directly leveraging B-Tree index traversal. Regardless of how deep into a billion-row archive the request jumps, database search operations execute with consistent latency.
- Eliminating Drift: Immutable structural keys ensure stable sequential navigation regardless of real-time dataset additions occurring ahead of the cursor point.
Production Implementation: Cursor Pagination via Python & SQL
import base64
import json
from datetime import datetime
from typing import List, Dict, Any, Tuple
import psycopg2
class CursorPaginator:
def __init__(self, db_connection):
self.conn = db_connection
@staticmethod
def encode_cursor(created_at: datetime, record_id: int) -> str:
payload = json.dumps({"ts": created_at.isoformat(), "id": record_id})
return base64.urlsafe_b64encode(payload.encode('utf-8')).decode('utf-8')
@staticmethod
def decode_cursor(cursor_str: str) -> Dict[str, Any]:
try:
decoded_bytes = base64.urlsafe_b64decode(cursor_str.encode('utf-8'))
return json.loads(decoded_bytes.decode('utf-8'))
except (ValueError, KeyError, TypeError) as e:
raise ValueError(f"Invalid cursor payload structure: {e}")
def fetch_paginated_events(self, limit: int = 20, cursor: str = None) -> Dict[str, Any]:
# Enforce hard defensive limits against arbitrary resource exhaustion
safe_limit = min(max(1, limit), 100)
# We fetch n+1 records to deterministically deduce whether another page exists
fetch_limit = safe_limit + 1
params = [fetch_limit]
where_clause = ""
if cursor:
cursor_data = self.decode_cursor(cursor)
# Tuple comparison leverages optimal PostgreSQL multi-column B-Tree indices
where_clause = "WHERE (created_at, id) < (%s, %s)"
params = [cursor_data["ts"], cursor_data["id"], fetch_limit]
query = f"SELECT id, event_type, created_at, payload FROM audit_logs {where_clause} ORDER BY created_at DESC, id DESC LIMIT %s"
else:
query = f"SELECT id, event_type, created_at, payload FROM audit_logs ORDER BY created_at DESC, id DESC LIMIT %s"
with self.conn.cursor() as cur:
cur.execute(query, params)
rows = cur.fetchall()
has_next_page = len(rows) > safe_limit
if has_next_page:
rows = rows[:safe_limit] # Slice off our probe record
formatted_data = []
for row in rows:
formatted_data.append({
"id": row[0],
"type": row[1],
"created_at": row[2].isoformat(),
"payload": row[3]
})
next_cursor = None
if has_next_page and formatted_data:
last_record = rows[-1]
next_cursor = self.encode_cursor(last_record[2], last_record[0])
return {
"data": formatted_data,
"meta": {
"limit": safe_limit,
"has_next_page": has_next_page,
"next_cursor": next_cursor
}
}
6. Rate Limiting: Mathematical Models & Algorithmic Design
API Rate Limiting defends system infrastructure against noisy neighbor degradation, Distributed Denial of Service (DDoS) attempts, and brute-force credential stuffing.
Comparison of Rate Limiting Algorithms
graph TD
RL[Rate Limiting Architectures]
RL --> TB["Token Bucket<br>• Burst friendly<br>• Smooth refill rate<br>• Memory: O(1) per user"]
RL --> LB["Leaky Bucket<br>• FIFO queue smoothing<br>• Constant downstream processing<br>• Drops on overflow"]
RL --> FWC["Fixed Window Counter<br>• Reset at window boundaries<br>• Flaw: 2x traffic bursts at edges"]
RL --> SWC["Sliding Window Counter<br>• Weighted window interpolation<br>• Excellent accuracy-to-memory ratio"]
1. Token Bucket Model
A bucket holds a finite capacity of tokens. Tokens are continuously recharged at a consistent replenishment speed of tokens/sec. When an incoming HTTP request arrives requiring tokens, if current capacity , the transaction is admitted and decodes to . Otherwise, it drops with 429 Too Many Requests.
- Mathematical Formulation for Current Tokens ():
- Why It Works: Allows short traffic bursts up to capacity while strictly bounding long-term average consumption rate to .
2. Sliding Window Counter Model
Fixed window limiters (e.g., max 100 requests per minute from 12:00:00 to 12:01:00) suffer from boundary convergence bursts: a client can execute 100 queries at 12:00:59 and another 100 at 12:01:01—flooding the backend with 200 operations in a 2-second span.
Sliding window algorithms solve this by mathematically weighting previous window occupancy against elapsed time within the current slot:
Production Implementation: Atomic Sliding Window Log via Redis Lua Script
To prevent distributed race conditions where concurrent worker nodes check limits simultaneously before decrementing registers, execution logic must run atomically inside Redis using a custom embedded Lua script:
-- File: rate_limiter_sliding_window.lua
-- KEYS[1] : User specific limiter key -> e.g., "ratelimit:user:usr_8402"
-- ARGV[1] : Current epoch timestamp in millisecond precision
-- ARGV[2] : Sliding Window evaluation threshold duration (e.g., 60000 ms for 1 min)
-- ARGV[3] : Maximum allowable concurrent hits within window (e.g., 100)
-- ARGV[4] : Unique payload token (UUID) representing current request atomicity
local limiter_key = KEYS[1]
local current_time = tonumber(ARGV[1])
local window_size_ms = tonumber(ARGV[2])
local max_allowed = tonumber(ARGV[3])
local request_id = ARGV[4]
local window_start_time = current_time - window_size_ms
-- 1. Prune outdated records lying entirely prior to active evaluation boundary
redis.call('ZREMRANGEBYSCORE', limiter_key, '-inf', window_start_time)
-- 2. Count existing valid transaction entries inside active window
local current_request_count = redis.call('ZCARD', limiter_key)
if current_request_count >= max_allowed then
-- Capacity threshold exhausted: compute exact ms duration until old tokens drop off
local earliest_record = redis.call('ZRANGE', limiter_key, 0, 0, 'WITHSCORES')
local retry_after_ms = (tonumber(earliest_record[2]) + window_size_ms) - current_time
return { 0, math.ceil(retry_after_ms / 1000) } -- Return rejected + Retry-After seconds
end
-- 3. Register current atomic interaction request into sorted set index
redis.call('ZADD', limiter_key, current_time, request_id)
-- 4. Extend key expiration to ensure cleanup of abandoned tracker registers
redis.call('PEXPIRE', limiter_key, window_size_ms + 5000)
local remaining = max_allowed - (current_request_count + 1)
return { 1, remaining } -- Return Accepted + Remaining Token Balance
7. Versioning Strategies: Comparative Analysis
As business capabilities mature, contract schemas drift. Breaking changes—such as mutating parameter types, stripping response keys, or tightening validation boundaries—require comprehensive versioning strategies to protect external integrations.
| Strategy | Pattern Implementation | Advantages | Disadvantages |
| :--- | :--- | :--- | :--- |
| URI Pathing | GET /v1/customers/c_123 | • Exceptionally clear implementation
• Easy routing in API Gateways
• Browser bookmarkable | • Violates semantic pure logic (URIs should represent entities, not historical specs)
• Requires route proliferation across components |
| Query Parameter | GET /customers/c_123?version=2 | • Simple default fallback mechanisms
• Preserves root resource identity | • Easily lost during HTTP redirects
• Degrades proxy caching rules |
| Custom Header | API-Version: 2026-08 | • URIs stay pristine and unchanged
• Decouples protocol payload from routing URL | • Difficult to execute quick command tests via standard web browsers
• Requires custom proxy cache configurations |
| Content Negotiation | Accept: application/vnd.org.customer.v2+json | • RFC-pure HTTP standards compliant
• Fine-grained schema variant bindings | • Highly complex parser logic on controllers
• Difficult for external consumers to debug |
Industry Verdict & Best Practice
For enterprise engineering (e.g., Stripe, Twilio, AWS), URI Pathing (/v1/) combined with Date-Based Version Headers is the industry gold standard. Major systemic version boundaries alter the URL (/v1/ to /v2/), while granular schema refinements rely on an explicit release timestamp header (OpenAI-Beta: assistants=v2 or Stripe's Stripe-Version: 2026-04-01).
8. Real-World Case Studies: Hyperscale Implementations
Stripe's Immutable API Architecture
Stripe manages backwards compatibility across thousands of financial client versions simultaneously. They achieve this without multiplying database tables or maintaining hundreds of separate controller scripts through an innovative Transformation Pipeline:
graph LR
C[Client V1 Request] --> GW[Stripe API Gateway]
GW --> M["Schema Transformations<br>(Apply upgrade diffs: V1 ➔ V2 ➔ V3)"]
M --> Core[Core Engine operates entirely on Latest V3]
Core --> Out["Response transformations<br>(Apply downgrade diffs: V3 ➔ V2 ➔ V1)"]
Out --> C
When a legacy integration submits a request using a 2020 version specification, the request passes through an automated cascade of stateless translation transformers that upgrade the payload to the latest active schema version. The database and domain logic process solely on the newest architecture. When responding, a mirrored reverse transformer strips and reshapes the response back to match the older 2020 schema contract.
Netflix API Gateway (Zuul & Spring Cloud Gateway)
At Netflix's operating scale, microservices do not speak directly to public clients. An extensible edge routing tier processes millions of routing decisions per second:
- Dynamic Routing: Dynamically routes requests based on real-time service discovery registrations.
- Shedding & Backpressure: During cascading network degradations, edge gatekeepers instantly inject default fallback JSON payloads for non-critical features (like recommended rating stars), protecting core video-streaming services from saturation.
- Security & Authorization Offloading: Validates, unpacks, and authenticates user tokens once at the external boundary, re-emitting lightweight internal cryptographically signed headers across internal infrastructure.
9. Advanced System Design Interview Questions & Rigorous Explanations
Q1: How do you architect an API endpoint designed to generate a heavy financial report that takes 45 seconds to compile, without breaching HTTP timeout boundaries?
Answer: Implement the Asynchronous Long-Running Operations (Polling / Webhook) pattern utilizing HTTP status code 202 Accepted.
- Initial Invocation: The client issues
POST /v1/reports/financial. - Synchronous Handoff: The controller generates a unique execution parameter ID (
job_9912), registers a pending state record inside an ACID datastore, pushes a processing task onto a reliable async worker queue (Kafka, RabbitMQ, AWS SQS), and immediately yields an HTTP response within < 50ms:HTTP/1.1 202 Accepted Location: /v1/reports/jobs/job_9912/status Retry-After: 5 - Status Polling: The client periodically hits
GET /v1/reports/jobs/job_9912/status, receiving{ "status": "PROCESSING", "progress": 45 }. - Final Resolution: Once background processing completes and saves the final PDF payload into object storage (S3), the status endpoint returns
303 See Otherwith a pre-signed S3 access link, or directly issues a200 OKJSON bundle containing download details.
Q2: Why is offset pagination considered an anti-pattern for large distributed transactional tables, and how does keyset cursor pagination internally alter general execution dynamics within relational database storage engines?
Answer: Offset pagination translates to SQL statements containing LIMIT K OFFSET N. To serve an offset of , relational DB engines cannot mathematically jump instantly to row position because rows vary in physical storage length and MVCC dead-row visibility constraints must be evaluated sequentially. The engine reads, decompresses, and discards all preceding rows before surfacing the required subset. This burns linear CPU cycles and I/O buffer pool memory, scaling at .
Conversely, keyset cursor pagination passes an absolute structural anchor reference (e.g., WHERE (created_at, id) < ('ts_val', id_val) ORDER BY created_at DESC, id DESC LIMIT K). Relational engines resolve this utilizing composite B-Tree traversal algorithms, traversing root-to-leaf nodes in or operations directly to the targeted index block, entirely ignoring preceding millions of historical records.
Q3: How do you protect a non-idempotent POST creation resource from generating duplicate transactions if a client experiences a TCP connection drop before receiving the successful 201 Created confirmation payload?
Answer: Implement Distributed Idempotency Tokens managed at the perimeter layer.
- The client generates a resilient UUIDv4 payload token (
Idempotency-Key: cb5a...) and transmits it alongside the operational body. - The edge server interceptor attempts an atomic reservation operation against an in-memory datastore using a strict locking primitive:
SETNX "idempotency:cb5a..." "LOCKED" TTL: 30. - If reservation fails (
SETNXreturns 0), the architecture identifies an actively processing duplicate or historical replay:- If marked
"LOCKED", return an error indicating concurrent operation in flight (409 Conflict). - If marked with a previously stored historical payload string, deserialize and directly re-emit the identical stored HTTP response code, headers, and body without executing downstream services.
- If marked
- If reservation succeeds, process the actual transactional state change, serialize the definitive response output, and overwrite the Redis token slot:
SET "idempotency:cb5a..." "{"status":201, "body":{...}}" EX 86400.
Q4: Explain the differences between optimistic vs. pessimistic concurrency locking within REST architecture when updating resource configurations via PUT or PATCH.
Answer:
- Pessimistic Locking relies on exclusive hardware or software transactional database locks (
SELECT ... FOR UPDATE), freezing all competing reads/writes until the active session commits. While secure against race anomalies, it violates REST statelessness paradigms, scales poorly across horizontal replicas, and induces operational deadlock risks under sustained high-concurrency loads. - Optimistic Locking remains stateless and architectural compliant by leveraging HTTP conditional semantics via Entity Tags (ETags). When retrieving a record via
GET /v1/documents/doc_1, the API emits a response header containing a cryptographic hash or revision counter:ETag: "rev_841". When modifying the entity, the client passes this identifier within anIf-Match: "rev_841"request header. The database issues an atomic update conditional statement:UPDATE documents SET content = 'new' WHERE id = 'doc_1' AND version = 'rev_841'. If an intervening worker modified the row beforehand, zero rows update, and the API returns412 Precondition Failedor409 Conflict, compelling the client to re-fetch the latest state and retry gracefully without blocking backend databases.