Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 7

Idempotency in API Design

Recommended resources

Introduction

In our last lesson, we examined API versioning strategies, focusing on how to evolve an API's "contract" without breaking client integrations. We established that an API is a promise of a stable interface. Today, we focus on making the execution of that contract reliable in the face of an unreliable world.

Network failures are a fact of life. A client might send a request to create a payment, but the connection might drop before the response is received. The client's natural reaction is to retry. But what if the first request actually succeeded? You could end up charging the customer twice.

This lesson tackles this critical problem. Your learning outcome is to explain idempotency in API design and describe techniques to implement it, such as using idempotency keys. Mastering this concept is non-negotiable for a senior backend role, as it demonstrates you can build robust, predictable, and safe systems.


The Problem: Unsafe Retries

To see why this is so important, let's start with a real-world example and a clear definition of the problem.

Idempotency in APIs: you should be aware of this!

The video 'Idempotency in APIs: you should be aware of this!' by Software Developer Diaries provides an excellent introduction. It starts with a real incident at Uber Eats that perfectly illustrates the danger of non-idempotent operations and then clearly defines the concept.

Please watch from the beginning to 02:54. Pay close attention to: The Uber Eats incident and how inconsistent error handling led to duplicate orders. The definition of idempotency: an operation that can be performed multiple times without changing the result beyond the initial application. The example of the client retrying a request and creating two burgers instead of one.

As the video explains, idempotency is the property that ensures making the same request multiple times has the same effect as making it once. It's a guarantee against unintended side effects from retries.

Some HTTP methods are idempotent by definition:

  • GET, HEAD, OPTIONS: These are safe methods that only retrieve data. Calling them multiple times doesn't change the server's state.
  • PUT: This method is designed to replace a resource entirely. If you PUT the same payload to /users/123 multiple times, the end state is the same as the first time.
  • DELETE: Deleting a resource at /users/123 multiple times has the same outcome. After the first call, the resource is gone, and subsequent calls will likely return a 404 Not Found, but the system state doesn't change further.

The challenge lies with POST and PATCH requests, which are not inherently idempotent. A POST to /orders is meant to create a new resource, so two identical POST requests will naturally create two identical orders. This is where we need an explicit mechanism to enforce idempotency.


The Solution: Idempotency Keys

The industry-standard solution is to use an idempotency key. This is a unique identifier generated by the client for a specific logical operation (like a single payment attempt). The client sends this key in an HTTP header, typically Idempotency-Key.

The server then uses this key to recognize and de-duplicate retried requests.

Idempotency Key Implementation Sequence Diagram
This sequence diagram illustrates the server-side logic. When a request arrives with an idempotency key, the server first checks if it has already processed this key. If yes, it returns the saved result. If no, it processes the request, saves the result, and then responds.

Let's break down the server-side workflow:

  1. Receive Request: The server gets a request (e.g., POST /payments) with an Idempotency-Key: <unique-uuid> header.
  2. Lock and Check: The server attempts to acquire a lock on this key and checks its storage to see if it has been processed before.
  3. Handle Scenarios:
    • New Key: If the key is new, the server processes the request (e.g., charges the card), saves the response (status code and body) associated with the idempotency key, releases the lock, and returns the response to the client.
    • Existing Key (Completed): If the key exists and its associated request is already complete, the server immediately returns the saved response without re-processing anything.
    • Existing Key (In-Progress): If the key exists but is still being processed by another request (e.g., the lock is held), the server should immediately respond with a 409 Conflict to indicate a concurrent request is underway.

This pattern transforms a non-idempotent POST into an idempotent operation, making retries safe.


Implementation Deep Dive: Trade-Offs in Storage

A key architectural decision is where to store the idempotency keys and their results. The two main choices are a fast in-memory cache like Redis or a durable database like PostgreSQL. This choice involves a classic trade-off between performance and durability.

To explore this in depth, we'll turn to a detailed article that breaks down both approaches.

Requests at Scale — Idempotency

The article 'Requests at Scale — Idempotency' is a comprehensive guide to implementing this pattern. It provides excellent detail on the Redis and database approaches, along with the pitfalls to avoid.

Please read the following sections. Focus on understanding the core mechanisms and the trade-offs: Fast cache approach (Redis-like): Start from this heading. Pay attention to the use of atomic commands like SETNX and the importance of using a lock with a Time-To-Live (TTL) to handle server crashes. Database-backed approach (Postgres unique constraint): Read this section to understand how to use a database table with a PRIMARY KEY or UNIQUE constraint to enforce idempotency durably. Fast caching (Redis) vs Database (Postgres): Study the summary table comparing Speed, Persistence, and Failure Recovery. This is perfect material for an interview discussion.

Let's summarize the key points for your interview discussion.

1. Redis-based Approach (Performance-First)

  • Mechanism: Use Redis's atomic SET key value NX EX <ttl> command.
    • NX (Not Exists): Ensures the key is only set if it's new. This atomically wins the "race" between concurrent requests.
    • EX (Expires): Sets a Time-to-Live (TTL) on the key. This is a critical safety measure. If your service crashes mid-processing, the lock will automatically expire, allowing a subsequent retry to proceed. Without a TTL, the lock could persist forever.
  • Pros:
    • Extremely Fast: In-memory operations provide very low latency.
  • Cons:
    • Lower Durability: If the Redis instance fails, you could lose idempotency records. This could allow a duplicate operation if a retry occurs after the failure. This risk is often acceptable for less critical operations or if you have a highly available Redis setup.
  • Best For: High-throughput APIs where performance is paramount and the small risk of data loss on failure is acceptable (e.g., webhook de-duplication, API rate limiting).

2. Database-based Approach (Durability-First)

  • Mechanism: Use a dedicated table (e.g., idempotency_records) with a PRIMARY KEY or UNIQUE constraint on the idempotency key column.
    • When a request comes in, you attempt to INSERT the key into this table within a transaction.
    • If the INSERT succeeds, you won the lock. Proceed with the business logic.
    • If the INSERT fails due to a unique constraint violation, another request has already processed this key. You can then query the table for the stored response.
  • Pros:
    • Highly Durable: The record is as durable as your database. It survives service restarts and crashes.
  • Cons:
    • Slower: Involves disk I/O and transactional overhead, resulting in higher latency compared to Redis.
  • Best For: Critical operations where correctness and durability are non-negotiable, even at the cost of performance (e.g., payment processing, order creation).

Advanced Considerations: Interview-Ready Talking Points

A senior-level interview won't stop at the basics. Be prepared to discuss edge cases and demonstrate a production-ready mindset.

Three Microservices Patterns — Circuit Breaker, Idempotency Keys ...

The article 'Three Microservices Patterns' has a fantastic section on the tricky questions interviewers ask about idempotency.

Read the section titled 'Edge Cases Interviewers Love to Ask About'. These are exactly the kinds of follow-up questions you can expect.

Here are the key edge cases and how to address them:

  • Payload Mismatch: What if a client sends the same key but with a different request body?

    • Solution: You must create a "fingerprint" of the request payload (e.g., a SHA256 hash of the canonicalized body) and store it alongside the idempotency key. If a subsequent request arrives with the same key but a different fingerprint, you must reject it with a 422 Unprocessable Entity or 409 Conflict error. This prevents clients from accidentally or maliciously mutating an operation.
  • Key Retention (TTL): How long should you store idempotency records?

    • Answer: There's no single right answer; it's a trade-off. A common practice is 24-48 hours, which is long enough to handle most network retry storms but short enough to prevent the storage table/cache from growing indefinitely. For financial transactions, this might be extended to match chargeback windows (e.g., 30+ days). Use a background job or a TTL index to clean up expired records.
  • Client-Side Generation: Who generates the key?

    • Answer: The client must generate the key. This ensures that only the client can define what constitutes a single "logical operation." The key should be a cryptographically random string like a UUIDv4 to prevent collisions.

Structuring Your Interview Answer

When asked "How would you design an idempotent payments API?", structure your response to showcase your deep understanding.

Interviewer: "Imagine you're building a POST /payments endpoint. How would you ensure that a client's network-related retry doesn't charge the customer twice?"

Here's how you could answer, inspired by the "Senior-Level Interview Answers" in the LINK resource:

"To prevent double charges, I'd implement idempotency using idempotency keys. The client would be responsible for generating a unique key—typically a UUIDv4—for each payment attempt and sending it in an Idempotency-Key header.

On the server-side, for a critical system like payments, I'd prioritize durability. I would use a PostgreSQL table with a unique constraint on the idempotency key. The flow would be:

*1. When a request comes in, I'd begin a database transaction and attempt to INSERT the key into my idempotency_records table.
*2. To handle potential payload changes on retries, I'd also compute a hash of the request body and include it in the insert. If a request arrives with a key that exists but has a different payload hash, I'd return a 409 Conflict.
*3. If the INSERT succeeds, my service "owns" the lock and proceeds with calling the payment gateway.
*4. If the INSERT fails due to a unique constraint violation, it means the request is a duplicate. I'd then query the table for the stored result of the original request and return that.
5. Upon successful processing, I'd store the response status and body in the record and commit the transaction. For failed processing, I'd roll back, allowing the client to retry with the same key.

Finally, I'd implement a cleanup job to purge records older than our business requirement, for example, 48 hours, to manage storage size."

This answer is strong because it states the pattern, justifies the choice of a durable store (Postgres for payments), and proactively addresses advanced edge cases like payload mismatch and record cleanup.


Conclusion

You now have a comprehensive understanding of idempotency, a critical concept for building reliable backend systems.

Key Takeaways:

  • Idempotency is the guarantee that retrying an operation produces the same result as the initial request.
  • It's essential for non-idempotent methods like POST to prevent unintended side effects from network retries.
  • The idempotency key pattern is the standard solution, where a client-generated key is used to de-duplicate requests on the server.
  • The implementation involves a trade-off between performance (Redis) and durability (Database) for storing keys and results.
  • Senior-level discussions require addressing edge cases like payload fingerprinting, in-progress requests, and key retention policies.

Preview of the Next Module:

We've now completed our module on Core Backend Architecture. You've learned about monoliths vs. microservices, REST vs. event-driven patterns, API versioning, and idempotency.

In the next module, Service Resilience and Fault Tolerance, we will build directly on these concepts. Our first lesson will be on rate limiting, another essential technique for protecting your APIs, but this time from being overwhelmed by too many requests, whether malicious or accidental.