I think this article (and the author's previous articles on their blog) is quite clearly AI written. It has such a frustratingly punctuated cadence and really does not serve the reader anything valuable.
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.
Idempotency is easy until the second request is different
161–170 of 244 posts
Re: Idempotency is easy until the second request is different
#162Earlier quoted context omitted.
This is just normal concurrent programming? If two requests come in for the same idempotency key/customer reference id, only one will succeed. Use standard database transaction isolation. So one will complete with 200, one will complete with 409. It doesn't matter which. That said, there's something odd about the way you phrased this question. If the original request hasn't gotten a response yet, why is it sending a…
So you have to serialize the requests, and have one of them wait for the other to finish to return the 409? > why is it sending a retry? may be two clients tries to do it? Or there's a bug with the client in how they do it? Isn't the point of idempotency meant to enable clients to retry again, without fear that a 2nd request somehow breaking things?
Not necessarily - there are different transaction isolation and conflict resolution methods provided by every database built for this purpose. You just have to ensure that only one request actually commits to the database, and that one sends a success response while the other sends a 409. The database or another lock provider can either help enforce serialization up-front - or the app can use optimistic locks based on data in the request that will only block if there is actually a conflict, and this won't delay the first transaction at all.
Solving these kinds of issues are exactly the purposes of idempotency keys and database transactions and using them in the intended way is really the only sound way to build a distributed system. Making things more complicated to "improve DevX" is just going to make them unsound. That is what Stripe chose to do. Their 24-hour replay idea is fine but why not send 409s after that rather than accept those transactions? If "that will never happen" then the 409s will never happen. It would have cost approximately nothing (if designed that way upfront) and inconvenienced their clients not at all.
Re: Idempotency is easy until the second request is different
#163Re: Idempotency is easy until the second request is different
#164Earlier quoted context omitted.
What if the original request is still being processed when the retry comes in? That doesn't fall into either of your categories: the request isn't successful, but it hasn't failed either.
Being charitable, I'd say the poster above is saying that in the web architecture you can (should?) shift more of the burden for idempotence to the client. But, rather than 409, I'd say that you should be using opportunistic concurrency control if you adopt this perspective. There should be a resource context for the request, so the client can obtain an ETag and send If-None-Match headers, and get a 412 response if t…
That doesn't mean that idempotency keys have to be used. You can certainly hash message content if that is documented behavior. That probably only makes sense when there is already some logical session or transaction identifier that makes dedupe semantics clear.
The system you propose might be sound and might be necessary in some systems, but I can't think of what they might be that wouldn't be better served by the simpler solution that is already widely used for this purpose.
Re: Idempotency is easy until the second request is different
#165Earlier quoted context omitted.
How would the requester get notified if it doesn’t know which request succeeded? Is it listening for events? And at the sake of repeating the above commenter, you solve the multiple server by serializing somewhere, because you ultimately need a lock on something. You can also perform the operation in both places and then reconcile the state later but that’s a lot more complex.
> How would the requester get notified if it doesn’t know which request succeeded? By sending a third request and getting a response that reveals the state of the system.
Re: Idempotency is easy until the second request is different
#166Earlier quoted context omitted.
If you're a client using the same idempotency key for a materially different request you have a bug.
Yes, and if you are building a payment API you need to be robust to client bugs.
Re: Idempotency is easy until the second request is different
#167Earlier quoted context omitted.
To be honest, I liked your original response about returning a 409 - it's not something I'd done before and I like how it keeps things simpler. But your follow up responses here are making me rethink. Now you have to have all these special cases where the original request is still in process. I think or assertion of "99% are simple POST operations" is bullshit. For the times where idempotency is hard and really matte…
> special cases where the original request is still in process This isn't a special case, and it's the same problem if you want to replay the original response on conflict. If the original request isn't complete, what are you going to replay ?
Re: Idempotency is easy until the second request is different
#168Earlier quoted context omitted.
That's usually solved with traditional database transactions. Even if you have a complex long-running multistep orchestration problem, you can break it down into simpler transactions. Eg you could start with a "lock the resources" txn. But 99% of these conversations around idempotence are simple POST operations like "create order" that regular old database concurrency management handles just fine.
To be honest, I liked your original response about returning a 409 - it's not something I'd done before and I like how it keeps things simpler. But your follow up responses here are making me rethink. Now you have to have all these special cases where the original request is still in process. I think or assertion of "99% are simple POST operations" is bullshit. For the times where idempotency is hard and really matte…
I.e. idempotent DELETE with proper protocol behavior requires that one request see the 200 OK or 204 No Content and the other sees 404 Not Found, because the delete has already happened. It would be misleading to say 200 OK to both, because that answer means the resource was there when the request arrived.
Honestly, the whole HTTP resource model has a different conceptual backing for state management than the independently developed "idempotence" concepts in distributed systems. Those non-HTTP concepts came from more message-based rather than resource-based architectural assumptions.
The cleanest mapping in the spirit of HTTP would be that you do multiple round trips. A POST creates a new idempotence context, a bit like "start a transaction". The new URI is the key for coordinating state change and allowing restart/recovery.
As I remember it, the idea of idempotence keys in headers really came from the SOAP RPC mindset. It's kind of funny to see it persisting in some hybrid SOAP + REST mental model.
Re: Idempotency is easy until the second request is different
#169Re: Idempotency is easy until the second request is different
#170Earlier quoted context omitted.
I'm not assuming anything. Let me try to reframe this for you. The case of "client sends a retry with the same idempotency key" generalizes to "multiple requests come in for the same idempotency key". These can come in spread out over time (like a traditional loop), or they could come in at once. The solution is the same either way. The problem of "how do we deal with multiple conflicting requests coming in at once"…
> I'm not assuming anything. Sure you are. You said: "Retries will only receive 409 if the original request was successful. If the original request failed, the server performs the operation as normal on the second request. It doesn't replay failures." I understand all that just fine; you don't need to keep trying to "reframe" it. But what you said that I just quoted above assumes, implicitly, that if you get a second…
Whether or not a prior request exists in the system in processed or unprocessed state should not matter in a properly implemented idempotent system, the whole point is that one and only one is processed, and all replicas indicate that they are such.
What you do inside of your boundary to implement that idempotent contract need not be part of the contract and the decision of what primitives to use (locking, content-based addressing etc) are mainly just a question of implementation constraints.