Earlier quoted context omitted.
> You still return 409 No no no no no. You have multiple clients submitting the same business operation simultaneously. One must succeed, the others must fail. If you're using the 409 approach ("notify client that request is redundant") you must not send a 409 code until the work is complete. The client must interpret 200 and 409 as success cases. 200 means "it was done" and 409 means "it was already done". Clients l…
> You have multiple clients submitting the same business operation simultaneously. It doesn't have to be multiple clients. It could be the same client, not having received a response to its first request and deciding to re-send the request again.
Idempotency is easy until the second request is different
221–230 of 244 posts
Re: Idempotency is easy until the second request is different
#222Earlier quoted context omitted.
I really like this approach, and am going to keep it in my back pocket. However, it's good for e-commerce where there are a subset of "important" operations, but I'd argue the idempotency key is better for a financial API like Stripe where the majority of your operations need these semantics. You also run into a problem if PUT/PATCH needs to be exactly-once instead of at least once. Not as common, but again, might be…
There's nothing special about any particular HTTP method; it is the inherent nature of distributed systems that you can not have exactly-once semantics with a single HTTP request. To create exactly-once semantics, you need a client which retries until success and a server which prevents duplicates. The only difference between the approach I'm describing and Stripe's approach is the detail of how the client knows that…
I think there is also a risk of an "easy API" when it leads to magical thinking and sloppy development. If the naive client programmer starts to think the reliability is handled for them, they may also flub the handling of the idempotence key that remains the crux here. E.g. not persisting them well enough and returning to a situation where the user can accidentally make duplicate payments just like the naive system with no idempotence feature...
Re: Idempotency is easy until the second request is different
#223Earlier quoted context omitted.
You still return 409, with error detail in the response body, because it is still true that the client sent multiple requests with the same idempotency key, regardless of the progress of the original request. Regardless, I think your assumption about how the request/response cycle should be working is wrong. For this kind of API and transaction, the server should be returning a response immediately: 202 Accepted. The…
By "request" I didn't necessarily mean the HTTP request sent by the client, and I don't think the post I was responding to did either. But I agree my use of terminology was ambiguous. Let me restate more clearly, and in a way that shows the issue under your request/response process: An HTTP request comes in with a certain idempotency key. The server returns 202, as you say, and begins to process the database transact…
You seem very focused on long-running orchestration type systems. You build these on top of basic transactional primitives, but it's a mistake to try to make the whole process a single transaction. You can have a quick, transactional "start process" operation which must be idempotent. Other operations like "check status" need not be so complicated.
You don't necessarily share the idempotency key between the "start process" request and the "check status" request. You could for convenience, but it isn't necessary, and on balance most APIs don't. This is the "client picks ID" vs "server picks ID" design choice.
Re: Idempotency is easy until the second request is different
#224Earlier quoted context omitted.
> The "create payment" operation must be transactional and the communication channel between you and the processor must be idempotent so you don't inadvertently create multiple payments. Yes, I agree. You want to generate a token, persist it locally and use that to communicate with the payment gateway, so re-submissions use the same key and either error or return the transaction state. > The fact that payments have a…
This is one possible API but it's not the dominant way payments are exposed by payments providers. Stripe, Amazon, Paypal, et al do this differently. They're fine.
Even then, it’s not fine because those requests might time out, or your request times out waiting for theirs. Just because your provider abstracts behind one API doesn’t mean you necessarily can!
Re: Idempotency is easy until the second request is different
#225Earlier quoted context omitted.
By "request" I didn't necessarily mean the HTTP request sent by the client, and I don't think the post I was responding to did either. But I agree my use of terminology was ambiguous. Let me restate more clearly, and in a way that shows the issue under your request/response process: An HTTP request comes in with a certain idempotency key. The server returns 202, as you say, and begins to process the database transact…
Best practice is to keep database transactions short. There is no value in returning a "hey this is in progress" error code to a client when transactions are short, and databases don't easily give you this primitive anyway. You seem very focused on long-running orchestration type systems. You build these on top of basic transactional primitives, but it's a mistake to try to make the whole process a single transaction…
Fair enough. So basically your approach is to wait until the first request completes to decide how to respond to the second request that came in with the same idempotency key.
However, that would seem to me to imply that when the second request comes in, you check its idempotency key, realize you've already received a request with that key and you're processing it, and don't do anything else with the second request until the first one is completed. In particular, you don't have the second request trigger the start of another transaction.
But elsewhere in this thread, you've said you would start a second transaction based on the second request, and let your database's transaction mechanism tell you that it's a duplicate when you try to commit it. Why would you do that if you've checked the second request's idempotency key and you know it's a duplicate?
> You seem very focused on long-running orchestration type systems.
I'm not focused on anything except getting what I thought would be a simple answer to a simple question. The above seems to provide that (though it still leaves a question open, as above). That's all I wanted.
> You don't necessarily share the idempotency key between the "start process" request and the "check status" request.
I'm not talking about a "check status" request. The scenario I've been asking about all along is when a second "start process" request comes in with the same idempotency key as a previous "start process" request, while the process is still in progress.
Re: Idempotency is easy until the second request is different
#226Earlier quoted context omitted.
Best practice is to keep database transactions short. There is no value in returning a "hey this is in progress" error code to a client when transactions are short, and databases don't easily give you this primitive anyway. You seem very focused on long-running orchestration type systems. You build these on top of basic transactional primitives, but it's a mistake to try to make the whole process a single transaction…
> There is no value in returning a "hey this is in progress" error code to a client when transactions are short Fair enough. So basically your approach is to wait until the first request completes to decide how to respond to the second request that came in with the same idempotency key. However, that would seem to me to imply that when the second request comes in, you check its idempotency key, realize you've already…
Part of the problem here is that we're confusing how do you structure the API (replay? 409? something else?) with how we implement the API. The original article (and my original response) focused on API structure. We're wandering into the details of implementation, which is fine, but there are of course many ways to do the implementation. Some simpler than others.
Here's the simplest and most reliable way to implement idempotency for a trivial "create payment" operation, where the client submits an idempotency key. This pattern is incredibly common. Every request looks something like this:
* Start a transaction
* Lookup "does this idempotency key already exist"
* If it doesn't, insert the payment record with the idempotency key
* Commit the transaction
* Return the result. Successful insert is always 200OK. "key already exists" results in either replay of the original result (Stripe model) or an explicit error like 409 (my favored approach, still ubiquitous in ecommerce, and very common in financial APIs that predate Stripe).
Does that help? If you're using your database to handle concurrency, you need every request to start inside the transaction. You can't check the idempotency key outside of the transaction or you can't guarantee once-and-only-once behavior.
[Before someone mentions it, yes you can use a unique constraint instead of an explicit transaction, and this is conceptually identical - the check-for-dup transaction is inside a single INSERT]
Re: Idempotency is easy until the second request is different
#227Earlier quoted context omitted.
This is one possible API but it's not the dominant way payments are exposed by payments providers. Stripe, Amazon, Paypal, et al do this differently. They're fine.
I pointed to Braintree in another comment where this is very much not fine. Also these providers operate at a lower level of the stack than we do, so they have finer control over the process than you or I. Even then, it’s not fine because those requests might time out, or your request times out waiting for theirs. Just because your provider abstracts behind one API doesn’t mean you necessarily can!
500 errors, network timeouts, etc all happen. We can't run 2PC transactions with Stripe, so you need durable retries. People run billions of dollars through these APIs every day. It's fine.
Re: Idempotency is easy until the second request is different
#228Earlier quoted context omitted.
You still return 409, with error detail in the response body, because it is still true that the client sent multiple requests with the same idempotency key, regardless of the progress of the original request. Regardless, I think your assumption about how the request/response cycle should be working is wrong. For this kind of API and transaction, the server should be returning a response immediately: 202 Accepted. The…
> You still return 409 No no no no no. You have multiple clients submitting the same business operation simultaneously. One must succeed, the others must fail. If you're using the 409 approach ("notify client that request is redundant") you must not send a 409 code until the work is complete. The client must interpret 200 and 409 as success cases. 200 means "it was done" and 409 means "it was already done". Clients l…
Re: Idempotency is easy until the second request is different
#229Earlier quoted context omitted.
What really does not serve the reader any value is this comment now appearing on nearly every single HN thread. (And neither does my comment, sorry about that.) If you like the article, upvote. If you don’t, don’t.
I appreciate when people tell me something is AI written, so I can avoid reading it.
Re: Idempotency is easy until the second request is different
#230Earlier quoted context omitted.
> There is no value in returning a "hey this is in progress" error code to a client when transactions are short Fair enough. So basically your approach is to wait until the first request completes to decide how to respond to the second request that came in with the same idempotency key. However, that would seem to me to imply that when the second request comes in, you check its idempotency key, realize you've already…
While I'd love to take credit for it, this isn't literally "my" approach; this is just the standard way that transaction processing systems on the web work. And it's so standard that you don't even have to write code for it. Databases handle all the isolation and concurrency issues for you. Part of the problem here is that we're confusing how do you structure the API (replay? 409? something else?) with how we impleme…
What you said up to that point didn't really. But then you said this:
> If you're using your database to handle concurrency, you need every request to start inside the transaction. You can't check the idempotency key outside of the transaction or you can't guarantee once-and-only-once behavior.
Which answers the question that what you said earlier in your post raised. If I'm understanding you right, "lookup the idempotency key" is also relying on the same database, so you need the whole operation to be inside a single transaction in that database.