API Design — How to Build Stable and Predictable APIs (Understanding Idempotency)

October 17, 2024

☕️ Support Us
Your support will help us to continue to provide quality content.👉 Buy Me a Coffee

In our previous article API Design — What Makes a Good API Design?, we discussed the characteristics of good APIs, including two critical aspects that deserve deeper discussion: compatibility and predictability. These are particularly important because when handled poorly, they can cause cascading problems throughout your system.

Today, let's deep dive into predictability and explore how to make your APIs behave consistently, even when things go wrong.

What Does Predictable Mean in API Design?

Think about the communication between your frontend and backend. Is it always stable? Of course not. User devices can malfunction, networks can drop intermittently, and timeouts can occur. So when designing APIs, you can't just focus on the "happy path" – you need to consider edge cases and failure scenarios.

Here's a question: When you're writing an email draft and click "Save" multiple times, should it create multiple drafts? When someone double-clicks "Checkout" on an e-commerce site due to network lag, should they be charged twice?

The answer to both is clearly no. But how do you ensure this doesn't happen?

Consider this real-world scenario: You're using an email client, finish writing a draft, and click save. Whether you click once or ten times, it should save the current state without creating multiple drafts. If clicking ten times due to network issues resulted in ten different drafts, the user experience would be terrible.

Or imagine a customer on an e-commerce site clicks "Checkout" once, waits, sees no response due to network issues, and clicks again. They shouldn't be charged twice. If this happened, no customer would trust that platform.

The Power of Idempotency

To achieve this stability, idempotency is crucial. Idempotency means that an API call or operation, no matter how many times it's performed, produces the same result. In other words, the API should have no side effects when called multiple times.

When you achieve idempotency, you ensure that retries don't cause unwanted duplicates, regardless of how many times the operation is attempted.

But what happens when APIs aren't idempotent? Let me share a story that cost a company millions.

A Million-Dollar Lesson: The Uber Eats India Incident

Gergely Orosz, a former Uber payments engineering manager, once publicly shared a major incident at Uber Eats in India. For a period of time, users linked to Paytm (one of India's largest payment providers) could place unlimited orders on Uber Eats, even when their Paytm accounts had zero balance.

How did this happen?

Paytm's API had always been idempotent, so Uber's payments team built their integration assuming this behavior. They didn't add extra safeguards because they trusted the API's consistency.

However, during what seemed like a harmless update, Paytm broke their API's idempotency. Here's what changed:

  • Before: When a user had insufficient funds, Paytm consistently returned the same error message
  • After: The first call returned the expected error, but subsequent calls returned a different error message

Two different error messages for the same scenario – seems harmless, right? Wrong.

The problem was that Uber's team had never encountered the second error message, so they hadn't programmed their system to handle it. When Uber's system received this unknown error, it defaulted to approving the transaction.

Once users discovered they could place orders by clicking twice when they had no balance, word spread rapidly across Indian universities. Uber Eats was flooded with free orders, causing significant financial losses.

How to Make Your APIs Idempotent

After that story, I'm sure you understand why idempotency matters. If Paytm's API had consistently returned the same error message for insufficient funds, this incident would never have happened.

So how do you make your APIs idempotent?

With RESTful APIs, some operations are naturally idempotent:

  • GET requests: Fetching the same resource multiple times returns the same data (assuming no changes occurred)
  • PUT requests: Since PUT replaces the entire resource, multiple identical requests result in the same final state
  • DELETE requests: Once a resource is deleted, subsequent delete attempts have no additional effect

But what about POST requests? These are trickier because they typically create new resources or trigger actions. A payment POST request, for example, should only process once, regardless of how many times it's called.

The Idempotency Key Solution

The most common solution is to use an idempotency key – a unique identifier that lets the server know whether a request has already been processed.

Here's how it works: When making a request that should be idempotent, the client includes a unique key. If the same request comes in again with the same key due to network issues or user double-clicking, the server recognizes it and doesn't process it again.

This is exactly what Stripe, one of the world's leading payment APIs, implements. Their API documentation includes an idempotency key field:

When calling a CreatePayment API, the client first generates an idempotency key (using something like a UUID). If the user clicks multiple times, all requests carry the same key, so the server knows the payment has already been processed and won't charge again.

Stripe published an excellent technical article titled "Designing robust and predictable APIs with idempotency" that explains this concept in detail. It's worth reading if you want to dive deeper.

Key Takeaways

To wrap up, understanding idempotency is essential for building reliable APIs. By implementing idempotency mechanisms, you can:

  • Prevent expensive failures: Non-idempotent APIs can cause financial losses and system instability
  • Ensure consistent user experiences: Users expect reliable behavior even when networks fail
  • Scale with confidence: As traffic increases, network issues become more frequent, making idempotency critical
  • Design robust distributed systems: Idempotency is fundamental to handling failures gracefully

When designing your next API, consider what happens when requests are duplicated. If duplicate requests could cause problems, implement idempotency keys or similar mechanisms to ensure predictable behavior.


Support ExplainThis

If you found this content helpful, please consider supporting our work with a one-time donation of whatever amount feels right to you through this Buy Me a Coffee page, or share the article with your friends to help us reach more readers.

Creating in-depth technical content takes significant time. Your support helps us continue producing high-quality educational content accessible to everyone.

☕️ Support Us
Your support will help us to continue to provide quality content.👉 Buy Me a Coffee