Idempotency vs Simplicity: Safe Retries vs Minimal Design

Overview This compares two competing goals when designing an operation or API: making it safe to repeat (idempotency) versus keeping it easy to build and reason about (simplicity). The tension matters because guarding against duplicate execution almost always adds state and logic that a minimal implementation would otherwise skip. Comparison Diagram IdempotencySimplicityClientreq #1retryServerkey store: A seenexecuted onceretries collapse to one resultClientreq #1retryServerexecutedexecuted againretries run twice, no dedupno key store, fewer moving parts Comparison Table Aspect Idempotency Simplicity Design intent Guarantee repeated execution has the same effect as one execution Minimize the number of moving parts and decisions in the implementation Handling duplicate requests Detects and ignores repeats using an idempotency key or natural key Processes each incoming request as new, with no duplicate detection State required Needs a dedup store (key, result, TTL) to remember prior executions Stateless with respect to prior calls, nothing extra to persist Behavior on client retry Safe to retry any number of times; result is unchanged Retry re-runs the operation, risking duplicate side effects Failure recovery Callers can blindly retry after timeouts without side-effect risk Callers must add their own checks before retrying after a failure Implementation cost Extra code for key generation, storage, locking, and expiry Fewer edge cases, less code, faster to build and review Testing burden Must cover concurrent duplicates, race conditions, and key expiry Test surface limited to the core logic path, no dedup scenarios Best-fit workloads Payments, distributed queues, webhooks, multi-step workflows Internal read-only endpoints, prototypes, low-stakes single-writer ops Key Differences Idempotency trades extra state for safety, simplicity trades safety for fewer parts Idempotent operations rely on a dedup key that a simple implementation has no reason to store Simplicity pushes retry-safety responsibility onto the caller instead of the server Idempotency adds testing surface for concurrency and expiry that simple code avoids entirely The right choice depends on whether duplicate side effects are tolerable for the operation When to Use Each Idempotency ...

September 6, 2026 · 3 min · 439 words · jeonck

Sync vs Async APIs: Blocking Calls vs Non-Blocking Callbacks

Overview A synchronous call blocks the caller until the server returns a result, tying up a thread or connection for the full round trip. An asynchronous call returns immediately with an acknowledgment and delivers the actual result later via a callback, event, or poll, letting the caller do other work in the meantime. Comparison Diagram Synchronous APIClientblocked - thread waitsrequest sentresponse receivedserver processingAsynchronous APIClientclient free: other workrequest sentcallback receivedserver working Comparison Table Aspect Sync API Async API Request initiation Caller invokes and immediately awaits the result on the same call Caller invokes and gets an immediate acknowledgment or handle (future, promise, message ID), not the result Response delivery Result returned in-line over the same connection/thread that made the call Result delivered later via callback, event, webhook, or by polling Caller behavior while waiting Thread or connection is blocked and cannot do other work Caller is free to continue other work or serve other requests Concurrency model Needs roughly one thread or connection per in-flight call A single thread or event loop can multiplex many in-flight calls Failure handling Errors surface immediately as exceptions or status codes at the call site Errors arrive out-of-band later and must be matched back to the original request Ordering and sequencing Strict: caller code executes in the exact order calls complete Responses can arrive out of order, requiring correlation IDs to reassemble sequence Latency impact on caller Caller’s total latency equals the full round trip Caller’s perceived latency is just the time to ack; real work overlaps with other tasks Implementation complexity Simpler code: straightforward call and return More complex: needs callback/promise/event handling and explicit state tracking Key Differences Sync calls block the caller until the response arrives, while async calls return control immediately. Async APIs scale better under load because they avoid thread-per-request limits inherent to blocking calls. Sync errors surface in-line at the call site; async errors require correlation back to the original request. Async responses can arrive out of order, adding sequencing complexity that sync calls never face. Sync code is easier to trace and debug since execution follows a single linear call stack. When to Use Each Sync API ...

September 6, 2026 · 3 min · 474 words · jeonck

Unary vs Streaming RPC: One Request-Response vs Continuous Message Flow

Overview Unary and streaming are the two call shapes gRPC (and similar RPC frameworks) support over HTTP/2. A unary call behaves like a classic function call — one request in, one response out, then done — while a streaming call keeps the connection open so either side can send multiple messages over time. The choice affects latency, backpressure handling, and how errors surface mid-exchange. Comparison Diagram Unary RPCStreaming RPCClientServerrequestresponseone call = one request + one response, then closedClientServermsg 1..None call = many messages over a long-lived connection Comparison Table Aspect Unary RPC Streaming RPC Call initiation Client opens the call and immediately sends the complete request Client opens the call, which may send zero, one, or many messages before or while reading responses Client-to-server messages Exactly one request message per call One (server-streaming) or many (client-streaming, bidi) messages per call Server-to-client messages Exactly one response message per call One (client-streaming) or many (server-streaming, bidi) messages per call Flow control Not needed — a single frame per direction fits within normal HTTP/2 windows HTTP/2 flow-control windows and backpressure govern how fast messages can be sent Connection/call lifetime Logically short-lived: opens and closes within one round trip Can stay open for the duration of a long-running exchange, sometimes indefinitely Latency and overhead Full connection/setup overhead paid per call since each call is independent Setup overhead amortized across many messages, lowering per-message latency Termination and errors A single status code ends the call atomically — it either succeeded or failed Status is sent only when the stream closes; errors can occur mid-stream after partial data was already delivered Key Differences Unary sends exactly one request and gets exactly one response, while streaming allows either side to send a sequence of messages over the same call Streaming relies on HTTP/2 flow control to manage backpressure across many frames; unary has nothing to manage A streaming call’s connection stays open far longer than a unary call’s brief request-response window Unary calls fail or succeed as a single atomic unit; streaming calls can deliver partial results before an error terminates them Streaming amortizes per-call overhead across many messages, cutting per-message latency compared to repeated unary calls When to Use Each Unary RPC ...

September 6, 2026 · 3 min · 482 words · jeonck

REST vs GraphQL: Multiple Endpoints vs Single Query Language

Overview REST structures an API as a fixed set of endpoints, each returning a predetermined shape of data tied to a resource. GraphQL exposes a single endpoint driven by a client-specified query, letting callers request exactly the fields they need across related resources in one round trip. Comparison Diagram RESTGraphQLClient/users/1/users/1/posts/posts/1/comments3 requests, fixed shapesResponse 1: full user objectResponse 2: full posts arraymay over- or under-fetch fieldsClient/graphql{ user(id:1){name posts{ title }} }1 request, client-shapedSingle JSON responsematches requested fields Comparison Table Aspect REST GraphQL Request entry point Multiple resource-based URLs (e.g. /users, /posts) Single endpoint (e.g. /graphql) for all operations Query specification Server defines response shape per endpoint Client defines response shape via query document Fetching related data Requires multiple round trips or ad-hoc nested routes Nested relations resolved in one request via resolvers Over/under-fetching Common — fixed payloads return unused or missing fields Minimized — client requests exactly the fields it needs Caching Leverages HTTP caching (ETags, CDNs, cache-control) Requires custom client-side or persisted-query caching Versioning strategy New versions (/v2/) or new endpoints for breaking changes Schema evolves additively; fields deprecated in place Error handling HTTP status codes signal success/failure per request 200 OK typical even on partial errors; errors array in body Tooling and discovery Relies on external docs (OpenAPI/Swagger) for contracts Self-describing schema with built-in introspection Key Differences REST models an API around resources and HTTP verbs; GraphQL models it around a typed schema and queries. REST responses have a shape fixed by the server; GraphQL responses are shaped by the client query itself. REST benefits from standard HTTP caching infrastructure; GraphQL typically needs bespoke caching layers. Fetching nested or related data usually takes REST multiple round trips, while GraphQL resolves it in a single request. REST signals failures through HTTP status codes; GraphQL usually returns 200 with errors embedded in the payload. When to Use Each REST ...

September 6, 2026 · 3 min · 428 words · jeonck

CQRS vs CRUD: Splitting Reads and Writes vs One Unified Model

Overview CRUD (Create, Read, Update, Delete) models an application around a single data model that handles both reads and writes through uniform operations. CQRS (Command Query Responsibility Segregation) instead splits reads and writes into separate paths, often with different models and stores optimized for each. The choice matters most as a system’s read/write patterns diverge or its business logic grows too complex for a single model to represent cleanly. Comparison Diagram CRUDCQRSClientModel / APIDatabaseone path, one modelClientCommandQueryWrite ModelRead ModelWrite DBRead DBsync via eventssplit paths, split models Comparison Table Aspect CRUD CQRS Request handling Single endpoint set (GET/POST/PUT/DELETE) hits one code path for both reads and writes Requests split into distinct Command (write) and Query (read) channels with separate handlers Data model One model represents the entity for both reading and writing Separate write model (domain/aggregate) and read model (denormalized view) per side Storage Single database or table serves both reads and writes Optional separate stores per side, e.g. relational for writes, cache or search index for reads Consistency Strongly consistent by default since reads hit the same store just written to Read side is often eventually consistent, synced from the write side via events Business logic placement Validation and rules scattered across create/update handlers Rules concentrated in command handlers that enforce invariants before state changes Scaling Read and write load scale together since they share the same path Read and write sides can be scaled and optimized independently Implementation overhead Minimal; straightforward to build, test, and reason about Higher; requires sync mechanism and handling of eventual consistency Key Differences CRUD uses a unified model for reads and writes; CQRS separates commands and queries into distinct paths CRUD read-after-write is immediate since storage is shared; CQRS’s read side is often eventually consistent CRUD business logic lives in generic handlers; CQRS pushes rules into explicit command handlers CQRS allows independent scaling of reads and writes; CRUD scales both together CRUD is simpler to build; CQRS trades that simplicity for flexibility at the cost of architectural complexity When to Use Each CRUD ...

August 4, 2026 · 3 min · 473 words · jeonck

REST vs GraphQL: API Query Model

Overview REST and GraphQL are both HTTP-based API paradigms that differ fundamentally in how clients request data. REST exposes multiple URLs with server-defined response shapes; GraphQL exposes a single endpoint where the client’s query body dictates exactly which fields and relationships to return. The distinction drives decisions around over-fetching, N+1 latency, HTTP cacheability, and schema contracts. Comparison Diagram RESTmultiple endpoints, fixed shapeClientGET /usersGET /postsGET /comments3 requestsEach response = full object:{ id, name, email, avatarUrl, createdAt, role, prefs }only name+email needed → over-fetchRelated data → N+1 round-tripsBreaking change → new URL, e.g. /v2/GET · cacheable · status codesGraphQLone endpoint, client-declared shapeClientPOST /graphql1 requestquery (client-authored)query { user(id: 1) { name email } posts { title }}SDL schema (server)type User { name: String! email: String posts: [Post]}Response: exactly what was declared{ user: { name, email }, posts: [{ title }] }Schema evolves via @deprecated, no versioningPOST · typed schema · introspectable Comparison Table Aspect REST GraphQL Endpoints One URL per resource type — GET /users, POST /orders, etc. Single URL (POST /graphql); resource selection is in the query body Response shape Fixed by the server; client receives all fields the endpoint defines Declared by the client per query; only the requested fields are returned Over/under-fetching Chronic: server returns full objects; client discards unused fields or must make extra requests for missing ones Eliminated by design: resolvers return only fields the query specifies HTTP caching Native: GET responses cache at CDN and browser level by URL + headers Non-trivial: queries are POST bodies; requires persisted queries or APQ for cache-key stability Type system Optional — enforced only if you add OpenAPI/JSON Schema; not validated at runtime by default Mandatory SDL schema; all queries are parsed and validated against it before execution Versioning Breaking changes typically require new URL paths (/v1/, /v2/) or custom Accept headers Additive schema evolution with @deprecated directives; a single endpoint surface stays stable Error handling HTTP status codes carry semantic meaning: 200, 404, 422, 500, etc. Always 200 OK; errors surface in an errors[] array alongside any partial data Introspection / discoverability Requires an external spec (OpenAPI/Swagger); not built into the protocol Built-in: query __schema or __type to get the full type graph at runtime Key Differences REST has one endpoint per resource; GraphQL has one endpoint for the entire API, and the query body — not the URL — determines what data comes back. REST responses return all server-defined fields for a resource (over-fetch), and fetching related data requires additional round-trips (N+1 problem). A single GraphQL query can traverse multiple types and return exactly the fields requested. REST uses standard HTTP verbs and status codes, making GET-based CDN and browser caching trivially available. GraphQL queries travel as POST bodies, breaking HTTP cache semantics by default and requiring explicit workarounds. GraphQL’s SDL provides a mandatory, introspectable type contract enforced at parse time. REST type contracts are optional add-ons (OpenAPI); the server can return anything without violating the protocol. REST breaking changes typically force a new URL version (/v2/); GraphQL prefers additive schema evolution with @deprecated, keeping a single endpoint surface over the API’s lifetime. When to Use Each REST ...

August 2, 2026 · 4 min · 726 words · jeonck