Opening

Imagine this scenario. A user opens your app, taps "Pay," and the network drops. The request was sent, but the app never received a response. Did the payment go through? The user does not know. The app does not know. The user taps "Pay" again. If the payment went through the first time, they just paid twice. If it did not, the second tap processes it correctly. The app has no way to distinguish these cases. This is the problem of idempotency, and it is one of the most critical concepts in building reliable software. An idempotent operation is one that produces the same result no matter how many times you execute it. Getting this right prevents double charges, duplicate records, and data corruption. Getting it wrong costs real money and real trust.

What Idempotent Means

Let me define it precisely. An operation is idempotent if performing it one time has the same effect as performing it multiple times. The key word is "effect" — we care about the state of the system, not just the return value. Setting a user's name to "Alice" is idempotent. Whether you do it once or ten times, the name is "Alice." Deleting a record with ID 42 is idempotent. The first time, the record is removed. The second time, there is nothing to remove — the system state is the same. Adding five dollars to a user's balance is NOT idempotent. Do it once: five dollars added. Do it twice: ten dollars added. The system state is different each time. This distinction is everything when it comes to safe retries.

HTTP Methods and Idempotency

HTTP was actually designed with idempotency in mind. Each method has a defined expectation. GET is idempotent. Fetching a resource does not change the server state. You can GET the same URL a hundred times and the server is unaffected. PUT is idempotent. It sets a resource to a specific state. PUT user/42 with name equals "Alice" — whether you send it once or ten times, user 42's name is "Alice." DELETE is idempotent. Deleting user/42 once removes it. Deleting it again changes nothing — it is already gone. POST is NOT idempotent. POST /orders with an order payload creates a new order each time. Two POSTs, two orders. This is why POST is dangerous to retry. PATCH is tricky — it depends on the implementation. A PATCH that says "set name to Alice" is idempotent. A PATCH that says "increment counter by one" is not. Most Android apps use POST for everything, which means most mutations are non-idempotent by default. That is a problem.

The Real-World Nightmare

Let me paint the full picture of why this matters. Your e-commerce app has a checkout flow. The user enters their credit card, taps "Place Order," and your app sends a POST request to your order API. Scenario one: the request reaches the server, the server processes it, creates the order, charges the card, sends the response — but the response is lost because the network drops. Your app sees a timeout. The order was created. The card was charged. But the app does not know. Scenario two: the request never reaches the server. Your app sees the same timeout. No order was created. Same symptom — timeout — but completely different reality. Without idempotency protection, you cannot safely retry. If you retry and the first request succeeded, you just created a duplicate order and a double charge. If you do not retry and the first request failed, the user's order is lost.

The Idempotency Key

The solution to this problem is the idempotency key. Here is how it works. Before sending a request, the client generates a unique identifier — a UUID typically — and includes it in the request as a header, commonly called Idempotency-Key. The server receives the request and checks: have I seen this key before? If no, process the request normally, store the key along with the response. If yes, do not process the request again — just return the stored response from the first time. From the client's perspective, you can safely retry as many times as you want. The first time, the server processes it. Every subsequent time, the server returns the cached result. The operation becomes idempotent even though the underlying logic is not. This is how Stripe handles payments. This is how most payment processors work. It is not optional for financial operations — it is mandatory.

How Stripe Does It

Stripe's implementation is elegant and worth understanding. When you create a payment intent, you can include an Idempotency-Key header. Stripe stores this key and the result for twenty-four hours. If you send the same request with the same key, Stripe returns the original result without processing the payment again. The key is scoped to your API key, so different merchants cannot collide. If the original request is still processing when you retry — meaning it has not completed yet — Stripe returns a 409 Conflict status, telling you to wait and try again. This prevents a subtle race condition where two requests arrive simultaneously. The twenty-four-hour window means the key eventually expires, which keeps the server's storage bounded. This pattern is battle-tested across millions of transactions daily.

Implementing Idempotency in Your API Layer

Let me walk you through implementing this on the Android side. First, generate the idempotency key when the user initiates the action — not when the network request fires. When the user taps "Place Order," generate a UUID and store it along with the order details. If the request fails and needs to be retried, use the same UUID. This is critical: the key must be tied to the user's intent, not to the individual request. If you generate a new key on each retry, you have defeated the purpose. In your Retrofit service, add the key as a header. You can use an OkHttp interceptor that attaches the key from a request tag. On the server side, store each key in a database or cache like Redis with the corresponding response. Before processing any non-idempotent operation, check if the key exists. If it does, return the stored response.

Client-Side Protection

Even without server-side idempotency support, you can protect against some duplicate operations on the client side. The simplest approach: disable the button after the first tap. When the user taps "Place Order," immediately disable the button and show a loading indicator. This prevents accidental double-taps. But this is not sufficient. The user can still navigate away and come back, or the app can restart during the operation. A more robust approach: track pending operations in a local database. Before starting an operation, write a record: "order in progress, key ABC." If the operation completes, update the record to "completed." If the app restarts mid-operation, check for pending records and handle them — either retry or check the server for status. This is more complex but prevents duplicate submissions across app restarts.

Which Operations Need Idempotency Protection

Not every operation needs idempotency protection. The cost of implementing it is not zero, so you need to be strategic. Definitely protect: payment processing, order creation, money transfers, account creation, any operation where duplication has financial or legal consequences. Probably protect: message sending, form submission, data mutation that users would notice if duplicated. Skip: read operations (already idempotent), analytics events (duplicates are tolerable), non-critical updates like updating a "last seen" timestamp. The rule of thumb: if a duplicate would cost money, lose data, or confuse the user, implement idempotency protection. If a duplicate is merely slightly wasteful, it is probably not worth the complexity.

The Retry-Safe Architecture

Let me tie everything together from the last three videos into what I call a retry-safe architecture. At the core: every mutation has an idempotency key. Every external call has a timeout. Every failed call is evaluated: transient or permanent? Idempotent or not? Transient and idempotent: retry with exponential backoff and jitter. Transient and not idempotent but has an idempotency key: retry safely using the same key. Permanent failure: surface the error, do not retry. Circuit breaker: if the same service fails repeatedly, stop trying. This decision tree handles every failure scenario gracefully. The user never gets a duplicate charge. The user never loses an order. And the app never gets stuck in an infinite retry loop.

Edge Cases to Watch For

Let me flag some edge cases that catch people off guard. First: idempotency key storage on the server needs a TTL. Without it, your storage grows forever. Twenty-four hours is typical for financial operations. Second: the key should be per-operation, not per-session. If the user places two different orders in the same session, they need different keys. Third: be careful with partial failures. If the server processes the request but fails while storing the idempotency key, a retry will process the request again — a duplicate. The key storage and the operation should ideally be in the same transaction. Fourth: client-generated UUIDs must be truly unique. Use UUID version 4, which is randomly generated with a collision probability so low it is practically zero.

Closing

Idempotency is not a nice-to-have. For any operation where duplication has consequences, it is essential. The pattern is straightforward: generate a unique key per user intent, send it with the request, have the server check for and store the key. But the mindset shift is bigger. You need to assume that every request might be sent more than once. Network is unreliable. Users double-tap. Apps crash mid-operation. Design every mutation to be safe to repeat, and you will never have to explain to a user why they were charged twice. In our final video in this phase, we are going to zoom out to the biggest trade-off in distributed systems: consistency versus availability, and what it means for your Android app's offline behavior.