Real architecture · Senior
Stripe: predictable APIs with idempotency keys
Real production architecture. Key points: Idempotent Operations, API Design Fundamentals, and Cloud Design Patterns.
Short answer
Real production architecture. Key points: Idempotent Operations, API Design Fundamentals, and Cloud Design Patterns. Every call between two nodes crosses a network, and networks fail in exotic ways, even the ones that are statistically reliable. Between a client and a server there are three distinct failures: the initial connection may not even be established; the call may drop midway, while…
01 · CONTEXT
The response that does not arrive
Every call between two nodes crosses a network, and networks fail in exotic ways, even the ones that are statistically reliable. Between a client and a server there are three distinct failures: the initial connection may not even be established; the call may drop midway, while the server is carrying out the operation, leaving the work in limbo; or the call may succeed and the connection drop before the server notifies the client.
Only the first failure is harmless: the client knows nothing happened and can retry. In the other two, the success of the operation is ambiguous from the client's point of view, which does not know whether retrying is safe. This holds for any distributed system, and the article points out that even the Stripe API, with a single other server calling it, already is one.
The client sends the charge request to the API server.
The server executes the operation and writes the effect to the database: the charge really happened.
The connection drops before the response arrives. From the client's point of view, nothing indicates whether the charge was made.
If the client retries with no protection, the server executes again and the charge happens twice.
The simplest way out is to design the server so that retrying is always safe. An endpoint with this property is called idempotente: it can be called any number of times with the side effects occurring only once. Faced with any error, the client retries until it gets a verifiable success, and its state converges with the server's.
02 · IDEMPOTENT BY NATURE
When retrying is already safe
The article uses as an example the API of a hypothetical DNS provider that creates a subdomain with a PUT. All the information needed to create the record goes in the call, so the client can invoke it as many times as it likes; if the server notices it is a duplicate because the domain already exists, it ignores the request and responds with success. By HTTP semantics, PUT and DELETE are idempotent, and PUT creates or replaces the entire resource with the contents of the body.
Stripe's documentation applies the same reasoning. In API v1, GET and DELETE are idempotent by definition: repeating them does not repeat the effects of the original operation, and sending an idempotency key on them changes nothing. POST requests, on the other hand, which change state, accept the key.
| Type of operation | Exemplo | Idempotency key? |
|---|---|---|
| PUT with the full state | Create a DNS record with all fields | Unnecessary, retrying is already safe |
| GET and DELETE | Read or delete a resource | No effect, idempotent by definition |
| POST that changes state | Charge a customer | Required, retrying would charge again |
03 · THE IDEMPOTENCY KEY
Exactly once, even with retries
And when the operation must happen only once? The article's example is a charge endpoint: calling it twice by accident would charge the customer twice. The idempotency key solves this. When making the request, the client generates a unique ID that identifies only that operation and sends it along with the normal body; the server correlates the ID with the request state on its side. If the client notices a failure, it retries with the same ID, and it is up to the server to decide what to do.
| Falha | What the server does on a retry |
|---|---|
| Failure to connect | Sees the ID for the first time and processes normally |
| Failure midway through the operation | If an ACID database rolled everything back, retries in full; otherwise recovers the state and continues |
| Failure on the response | Responds with the cached result of the operation that succeeded |
Note that the article does not prescribe a single behavior: what happens in the middle case depends on the implementation, which is why the choice of storing the state of each request matters so much.
The client generates a unique key for the operation and sends it along with the normal request body.
The idempotency layer checks the registry. The key has never been seen, so the request proceeds to execution.
The operation runs and the server stores the status and response body in the registry, after the endpoint execution begins (API v1 rule).
The response is lost on the way and the client is left not knowing the result.
The client retries with the same key. The layer finds the saved response and returns it, without calling the operation again.
If the retry arrived with parameters different from the originals, the layer would reject it with an error instead of executing.
The client generates the key. How to generate it is up to the client, but the documentation suggests UUIDs v4 or another random string with enough entropy to avoid collisions. Keys can be up to 255 characters and should not carry sensitive data, such as emails or personal identifiers. A second strategy is to derive the key from an object tied to the user, such as a shopping cart ID, which protects directly against double submission.
04 · WHAT THE SERVER DECIDES
Execute, return, or reject
For each retry the server has to choose between returning what it already has, rejecting, or actually executing. Stripe's documentation defines the API v1 cases, which the table below gathers.
| Retry situation | What happens |
|---|---|
| Same key and same parameters, after execution has begun | Returns the saved status and body of the first request |
| Same key with different parameters | Error, to prevent misuse of the key |
| Invalid parameters or conflict with another request in progress | Nothing is saved, and the request can be retried |
| Conflict with another request, possibly using the same key | Response with status 409, conflict |
| Key already removed and reused | May execute a new request |
The client can tell a response replayed by the server by looking for the Idempotent-Replayed header with a true value.
05 · KEY LIFECYCLE
Retry window and reconciliation
The key does not live forever. Stripe retains keys for at least 24 hours and may remove them after that, and reusing a removed key may execute a new request. That is why the guidance is to limit retries of an unresolved operation to the first 24 hours after the original request, always with the same key and the same parameters.
If the result is still unknown after that window, do not retry, neither with the old key nor with a new one. Instead, fetch the relevant object, inspect the request in the dashboard, and use webhook events to reconcile the result.
Another practical rule: if retries span separate SDK calls or process restarts, the application should generate and persist the operation's key and supply it on every attempt.
Before the first attempt, the application generates the operation key and persists it.
The request goes unanswered. While the window allows, the application retries with the same key and the same parameters.
If the process restarts midway, the application reads the persisted key instead of creating another.
Once the window passes with no known result, the application stops retrying, neither with the old key nor with a new one.
Reconciliation fetches the object and reads the webhook events, matching what happened with the local records.
06 · RETRY POLITELY
Exponential backoff, jitter, and the three rules
Retrying also has a cost. If the server is in an incident with real unavailability, retries do not get through and can worsen the degradation. That is why the recommendation is exponential backoff: the client waits briefly after the first failure and, as the operation keeps failing, waits a time proportional to 2^n, where n is the number of failures. Even so, if many clients failed at almost the same time, the retry schedules can line up and hit the server together, the so-called thundering herd problem. The fix is to add a random jitter to each client's wait.
Stripe's Ruby library does this on its own: it automatically retries on failure, with the same idempotency key, increasing wait times, and jitter.
| Stripe-Should-Retry header | What the client does |
|---|---|
| Verdadeiro | Retries using exponential backoff, so as not to overload the API |
| Falso | Does not retry |
| Ausente | Decides based on the status code and other information in the response |
Frequently asked questions
What is Stripe: predictable APIs with idempotency keys?
Real production architecture. Key points: Idempotent Operations, API Design Fundamentals, and Cloud Design Patterns.
What does the Stripe: predictable APIs with idempotency keys design cover?
It is organized in these sections: The response that does not arrive, When retrying is already safe, Exactly once, even with retries, Execute, return, or reject, Retry window and reconciliation, and Exponential backoff, jitter, and the three rules.
How long is Stripe: predictable APIs with idempotency keys and what level is it?
It takes about 17 minutes to read and is rated Senior level.
Now design it yourself
Run this case as an interview in the System Design school: you make the decisions, the tutor challenges them and you see how your design scores.