Idempotent APIs Explained ๐Ÿ” | Prevent Duplicates in Distributed Systems

Designing Idempotent APIs in Distributed Microservices

One of the earliest โ€” and most painful โ€” lessons you learn when working with distributed systems is this:

The same request will almost certainly be sent more than once.

Not maybe. Not sometimes.
Definitely.

Retries are not edge cases in distributed systems; they are a fundamental reality. Networks fail, clients time out, services restart, messages get replayed, and users click buttons more than once. If your system assumes a request is processed only once, it will eventually break โ€” usually in production.

This is where idempotency becomes a critical design principle.


The Real-World Problem We Faced

In one of our data-heavy microservice applications, everything looked fine during testing. But once real users started interacting with the system, we began noticing something alarming: duplicate records appearing in the database.

At first, it seemed mysterious. There were no obvious bugs or concurrency issues in the code. But after digging deeper, the root causes turned out to be very ordinary:

  • Users double-clicking the submit button
  • Slow or unstable network responses
  • Clients retrying requests after timeouts
  • Backend services processing the same request more than once
  • Race conditions between concurrent requests

None of these were exotic failures. They were completely normal behaviors in real-world systems.

And together, they exposed a hard truth: our APIs were not idempotent.


Understanding the Race Condition Trap

A race condition occurs when two or more requests attempt to modify the same state at nearly the same time, and the final outcome depends on the order of execution.

In our case:

  • Two identical requests arrived milliseconds apart
  • Both passed validation
  • Both wrote data
  • The system happily created duplicates

From the systemโ€™s perspective, nothing was โ€œwrong.โ€
From the business perspective, it was a disaster.

This is why idempotency is not just an optimization โ€” itโ€™s a correctness requirement.


First Line of Defense: The UI (And Why Itโ€™s Not Enough)

Our initial instinct was to fix the issue at the user interface level. The simplest solution was also the most obvious:

  • Disable the submit button after the first click
  • Show a loading indicator
  • Prevent accidental double submissions

This helped. A lot.

But it didnโ€™t solve the problem.

UI-based defenses improve user experience, but they do not guarantee correctness. Users can refresh pages, scripts can bypass UI logic, mobile apps can retry automatically, and network issues can still cause duplicate requests.

In distributed systems, the backend must assume the UI will fail.


Moving the Responsibility to the Backend

Real correctness lives on the server side.

Our first backend approach was straightforward:

  • Generate a unique idempotency key per request
  • Store that key in application memory
  • Reject duplicate requests seen within a short time window

This worked well for a proof of concept. It allowed us to detect and block duplicates quickly.

But it also had serious limitations:

  • Application memory is not shared across instances
  • Restarts wiped all stored keys
  • Horizontal scaling broke the guarantee
  • Memory-based tracking doesnโ€™t survive failures

In short, it was not production-grade.


Scaling Idempotency with Redis

To make idempotency reliable, we moved the logic to Redis, which we were already using in the system.

This immediately solved multiple problems:

  • Fast access for high-throughput APIs
  • Distributed across all service instances
  • Resilient to application restarts
  • TTL support to automatically expire old keys
  • Centralized consistency for retries

The flow became simple and robust:

  1. Client sends a request with an idempotency key
  2. Backend checks Redis for that key
  3. If the key exists โ†’ return the previous result
  4. If not โ†’ process the request
  5. Store the result and mark the key as completed

This ensured that the same request always produced the same outcome, no matter how many times it was retried.


Why Idempotency Is Non-Negotiable

In many systems, retries are not optional โ€” they are built into the architecture.

Consider real-world scenarios:

  • Payment gateways retry on network failure
  • Message queues replay messages
  • Clients retry automatically after timeouts
  • Services restart mid-processing
  • Event-driven systems reprocess events

Without idempotency:

  • The same payment could be charged multiple times
  • The same seat could be booked by multiple users
  • Inventory could be oversold
  • Data pipelines could corrupt downstream systems
  • Users lose trust โ€” fast

Once trust is lost, itโ€™s extremely hard to regain.


Idempotency Is About Business Safety, Not Just Tech

Idempotency is often discussed as a technical concept, but its impact is deeply business-critical.

Finance, bookings, inventory, healthcare, and logistics systems all rely on the assumption that retries wonโ€™t cause harm.

That assumption only holds when idempotency is designed into the backend from day one.


Common Mistakes to Avoid

Many teams fall into the same traps:

  • Relying on HTTP methods alone (POST vs PUT)
  • Assuming retries wonโ€™t happen
  • Trusting the UI to prevent duplicates
  • Using in-memory storage for idempotency
  • Ignoring race conditions under load

Idempotency must be explicit, persistent, and deliberate.


Key Takeaway

Retries are inevitable in distributed systems.
Duplicate processing is not.

Idempotency is not an optimization or a โ€œnice to have.โ€
It is a mandatory correctness guarantee.

If your API changes state, the backend must be idempotent โ€” regardless of client behavior, network failures, or system restarts.

APIs must be idempotent, and that responsibility belongs to the backend.

Post Comment