Background
Sections
IntroductionRequirements & Problem AnalysisConstraints & AssumptionsEstimation TechniquesFunctional vs Non-Functional RequirementsMoSCoW PrioritizationSystem Design FundamentalsArchitecture DiagramClass DiagramComponent DiagramData Flow Diagram (DFD)ER Diagram (Entity-Relationship Diagram)High Level Design (HLD)Low Level Design (LLD)Sequence DiagramState DiagramUse Case DiagramData StorageDocument StoresFile StorageGraph DatabasesIn-Memory DatabasesKey-Value StoresNewSQLNoSQL DatabasesObject StorageSQL Databases (RDBMS)Time-Series DatabasesWide-Column StoresDatabase ConceptsACID PropertiesCAP TheoremConsistency ModelsIndexingNormalization & DenormalizationReplicationSharding & PartitioningTransactions & Isolation LevelsScalabilityAuto-Scaling & ElasticityConsensus & Leader ElectionLoad BalancingReplication & Read ReplicasSharding & PartitioningVertical vs Horizontal ScalingAvailability & ReliabilityBackup & Data DurabilityCircuit BreakerData ConsistencyDisaster RecoveryFault Tolerance & FailoverGraceful DegradationHigh AvailabilityNetworkingCDNDNSFirewalls & VPNHTTP & HTTPSLoad Balancer & Reverse ProxyTCP/IP & UDPWebSocketsCachingCache InvalidationCache Read/Write PatternsCaching LayersEviction PoliciesRedis vs MemcachedMessaging & CommunicationDead-Letter QueueIdempotencyKafka vs RabbitMQ vs SQSMessage QueuesPub/SubCompute & ServicesAPI GatewayContainers & OrchestrationMonolith vs MicroservicesServerlessService DiscoveryService MeshWeb Server & App ServerAPI DesignAPI Versioning & IdempotencyAuthentication & AuthorizationGraphQLgRPCPaginationRate Limiting & ThrottlingRESTSecurityAuthentication & AuthorizationData PrivacyEncryptionInput Validation & InjectionOAuth2 & JWTSecrets ManagementXSS & CSRFStorage & File SystemsBackup & RetentionBlock vs File vs Object StorageData Lakes & WarehousesDistributed File SystemsEphemeral StorageObservability & MonitoringDistributed TracingHealth ChecksLoggingMetricsSLI, SLO, SLADesign PatternsBulkhead PatternCircuit Breaker PatternCreational PatternsRate Limiter PatternRetry PatternStructural & Behavioral Patterns

API Versioning & Idempotency

7 min read

In a Nutshell

Two API concerns that determine how gracefully your API evolves and how safely clients can call it. Versioning is how you change an API without breaking the clients that already depend on it — because once someone integrates with your API, you can't just change its shape out from under them. Idempotency (in the API context) is how you let clients safely retry requests after a network failure without causing duplicate side effects — a client that times out on POST /charges must be able to retry without double-charging. Both are about the reality that APIs are contracts consumed by clients you don't control, over networks that fail.

2D minimalistic diagram split in two: left labeled "Versioning" shows an API serving /v1 to old clients and /v2 to new clients simultaneously without breaking either; right labeled "Idempotency" shows a client retrying a payment request with the same idempotency key, and the server recognizing the duplicate and charging only once

How It Actually Works

Why Versioning Is Necessary

You ship an API. Clients integrate. Now you need to change it:
  - rename a field, change a type, restructure a response, remove an endpoint

If you change it in place → every existing client BREAKS. 💥
Versioning lets old clients keep using the old contract while new clients
adopt the new one — evolve without breaking anyone.

Breaking vs Non-Breaking Changes

The first rule: know which changes force a new version.

Non-Breaking (usually safe) Breaking (needs a new version)
Adding a new optional field Removing or renaming a field
Adding a new endpoint Changing a field's type
Adding an optional parameter Making an optional field required
Adding an enum value (carefully) Changing response structure
Changing error codes/semantics

Design clients to tolerate additions (ignore unknown fields) so additive changes never require a version bump.

Versioning Strategies

Strategy Example Notes
URI path /v1/orders, /v2/orders Most common, explicit, cache-friendly
Query param /orders?version=2 Simple but easy to omit
Header Accept: application/vnd.api.v2+json Clean URLs; less visible/discoverable
Content negotiation Media type versioning RESTful purist choice

URI path versioning is the pragmatic favorite — explicit, visible, easy to route and cache. Whatever you choose, publish a deprecation policy: how long old versions live, how you communicate sunsets, and a migration path.

Idempotency for Safe Retries

The problem (see Idempotency): a client sends POST /charges, the server processes it, but the response is lost. The client can't tell "failed" from "succeeded-but-ack-lost," so it retries — risking a duplicate charge.

The solution: idempotency keys. The client generates a unique key per logical operation and sends it with every retry; the server dedupes.

POST /charges
Idempotency-Key: 7f3a-unique-per-charge      ← same on every retry

Server:
  if key seen before → return the STORED original response (no re-charge)
  else               → process, store (key → response) atomically, return

This is how Stripe makes charges safe to retry. Which HTTP methods are naturally idempotent matters too:

Method Idempotent? Retry Safety
GET, PUT, DELETE, HEAD Yes (by spec) Safe to retry
POST, PATCH No Needs an idempotency key

Combining Both: Evolving Safely + Retrying Safely

Versioning and idempotency are the two pillars of a robust, client-friendly API contract: versioning lets the API change over time without breaking clients; idempotency lets clients interact reliably over an unreliable network. Together they make an API something teams can build on with confidence.

2D minimalistic diagram showing the idempotency-key flow for a payment: a client sends POST /charges with key "abc"; the server processes and stores key+response; the response is lost in the network; the client retries with the same key "abc"; the server finds the stored result and returns it without charging again, guaranteeing a single charge

Seeing It in Action

Scenario: Versioning an evolving payments API and making charges retry-safe.

Versioning — introducing a breaking change:
  v1 response:  { "amount": 1995 }              (cents, integer)
  Business wants: structured money with currency.
  v2 response:  { "amount": { "value": 19.95, "currency": "USD" } }
                 ← BREAKING: shape and type changed.

  Strategy:
    - Serve BOTH: /v1/charges (unchanged) and /v2/charges (new shape).
    - Old integrations keep working on v1; new ones adopt v2.
    - Announce v1 deprecation with a 12-month sunset + migration guide.
    - Add a `Deprecation` + `Sunset` response header to v1 responses.

Idempotency — safe retries on charge creation:
  POST /v2/charges
  Idempotency-Key: order-7-attempt   ← stable per logical charge
  { "amount": { "value": 19.95, "currency": "USD" }, "source": "tok_..." }

  Server logic (atomic):
    try: INSERT idempotency_keys(key, status) VALUES('order-7-attempt','new')
    except UniqueViolation:                # duplicate retry
        return stored_response(key)        # original result, no double charge
    result = charge_gateway(...)
    store_response(key, result)            # same transaction
    return result

What happens on a flaky network:
  attempt 1 → charged, but 200 response LOST → client sees timeout
  attempt 2 → same Idempotency-Key → server returns the stored charge result
  → customer charged exactly once, client gets a clean success. ✅

Why both matter together: the versioning strategy means the payments team can improve the money representation (a genuinely breaking change) without a single existing integration breaking — old clients ride /v1 until they migrate on their own schedule. The idempotency key means that even over the unreliable networks real clients use, a retried charge never double-bills a customer. One concern protects clients across time (as the API evolves); the other protects them across failures (as requests are retried). An API that handles both is one that external teams can trust with money — which is the whole point.

Interview Questions

  1. Q: What distinguishes a breaking from a non-breaking API change? Hint: Non-breaking (additive): adding an optional field, a new endpoint, or an optional parameter — existing clients keep working if they ignore unknown fields. Breaking: removing/renaming a field, changing a type, making an optional field required, restructuring responses, or changing error semantics — these force a new version because they invalidate existing client assumptions.

  2. Q: Compare the main API versioning strategies. Hint: URI path (/v1/orders) — explicit, visible, cache/route-friendly, most common. Query param (?version=2) — simple but easy to omit. Header/content negotiation (Accept: ...v2+json) — clean URLs but less discoverable/visible. URI path is the pragmatic favorite. Whatever you pick, pair it with a clear deprecation/sunset policy and migration path.

  3. Q: Why do clients need idempotency keys for POST requests? Hint: POST isn't idempotent, and networks fail — a client can't distinguish "request failed" from "succeeded but the response was lost," so it retries, risking duplicate side effects (double charge). An idempotency key (unique per logical operation, sent on every retry) lets the server detect the duplicate and return the original stored response instead of reprocessing — making retries safe.

  4. Q: Which HTTP methods are idempotent, and how does that affect retry logic? Hint: GET, PUT, DELETE, HEAD are idempotent by spec — safe to retry directly since repeating them yields the same result. POST and (usually) PATCH are not — retrying can create duplicates or apply changes twice, so they need an idempotency key or conditional logic to be retry-safe. Clients/proxies can auto-retry idempotent methods but must be careful with POST.

  5. Q: How do you roll out a breaking change without disrupting existing clients? Hint: Serve both versions simultaneously (e.g., /v1 unchanged, /v2 with the new shape), let existing clients stay on the old version, announce a deprecation timeline with a sunset date and migration guide, add Deprecation/Sunset headers to old responses, and monitor old-version usage to know when it's safe to retire. Never change an existing version's contract in place.

References

Dive Deeper