6 ms·
This is vital if you are designing apis or clients that deal with charging a user money. It should be literally impossible for a user to accidentally get charge
by ageitgey 8y ago
This is vital if you are designing apis or clients that deal with charging a user money. It should be literally impossible for a user to accidentally get charged twice due to a flakey connection if you design correctly.
The trick is to have the client generate a random 'idempotency key' (a uuid) to start each logical transaction and have the server use that id to prevent double charges of the same transaction. By always passing that key, client can request that the payment be processed a 100 times with no fear of it being processed more than once.
This stripe blog post has as good a description as any: https://stripe.com/blog/idempotency https://stripe.com/blog/idempotency
- rectangletangle 8y agoThis is solid advice. Another common trick is to disable the submit event when the submit button is clicked for the first time, preventing two requests from firing, when the user double clicks. Then re-enable the event, if the request fails. Ideally this is done in addition to server side nonce validation, and not as the only preventative measure, because browser differences, or network issues could cause a double request (more likely if the HTTP GET method is used, instead of a proper POST). Regardless server and client side techniques should be used together to create a better UX, similar to using both client and server side email format validation.
- deleted 8y ago[deleted]
- PinkMilkshake 8y agoThat's only a partial solution. It may be a user agent or a queuing system (or anything else sitting between a person clicking the button and where the final transaction is recorded) that attempts a retry.
- digitaLandscape 8y agoI don't like this because in practice sites usually fail to re-enable the submit button if something goes wrong. Just let the user submit multiple requests if they want to retry, don't take that away from them, just make it harmless.
- jen729w 8y agoNewbie programmer here, but even I am already implementing finite state machines which handily fix this sort of issue. Any sort of UI without them now feels archaic.
- hood_syntax 8y agoAgree 100%. If you don't have explicit state transitions in a UI, you're asking for trouble. I think some people don't realize how much harder they make it on themselves by not setting clear boundaries; they see the up front cost and balk, when it saves a non-trivial amount of headache in the future, not to mention reducing mental overhead when you've got the structure hashed out.
- digitaLandscape 8y agoMeh. The logic still applies: on a bad network, the first request can hang for half a minute when a retry would get lucky and go through immediately. Don't handcuff your users for no benefit, just because you can't write a well-behaved backend.
- BoiledCabbage 8y agoRestricting what the client can do isn't idempotency. Allowing the user to perform the same action (make the same api-call) multiple times, Nd your API coalesce them as if it were 1, is. The point and benefit of idempotency on this scenario is that you silently support "bad" behavior.
- notatoad 8y agodisabling the submit button is a nice UI feature to indicate idempotency, but it's not the same thing as the function actually being idempotent. both are important.
- qoi2ijds0 8y agoIMO disabling buttons causes more trouble than it's worth - you can never guarantee the user hasn't clicked twice (because don't forget the code that disabling the button in the first place is relying on an event firing that tells you the button was clicked - this doesn't mean 2 events can't be queued before the event handler is called), and then you need a whole chunk of code around re-enabling the button depending on what has happened after the fact which is then a big source of bugs.
- genericid 8y agoThis is basically client-side validation.
- tankerdude 8y agoYup. This is actually an interview question that I go through to see if they know mechanisms to prevent the double charge, fulfillment, etc. problem.
- rooam-dev 8y agoGood point, but I'd add that not only when charging money, but any operation that can have side effects if executed more than once (emails, external calls, etc.). Having idempotent queue consumers/listeners can help with overall systems resilience (someone already mentioned this below). It helps as much as you don't have to worry when restarting a queue broker and/or apps with consumers. It adds complexity, but I think it pays off in the long run.
- enraged_camel 8y agoI apologize for the noob question, but... is it a good idea to have the client be in charge of generating UUIDs? When dealing with idempotence, they have to be truly random, not just pseudorandom, right? I looked into this recently, and my understanding (from the cursory amount of research I did) was that the UUID generation method depends on the random number generator used by the browser, which can vary a lot. Why not generate the UUID on the server and send it to the client along with the page request (if using SSR)? I.e. the server generates the UUID, sticks it in a database field, then sends it to the client. When the client responds with the UUID, you can check that against the database to make sure its valid.
- chaboud 8y agoYou can do a mix of the two to reduce server traffic (especially for abandoned sessions) and user latency. Give the client a client ID that you generate that should be properly random... Have them submit that ID along with their self-generated ID.
- hardwaresofton 8y agoSo I think this is actually the secret to creating actually dependable, no-downtime transitioning endpoints. It's just an idea that has been rolling around in my head but: - Express all operations as log messages (ez pz distribution) - Ensure all operations are idempotent - Record the operations (this is the log you can distribute if you please) - Disallow API code modification, only allow accretion/use of new API endpoints. - All APIs that come up have their own databases, a bit of the CQRS model here (but without events -- just the actions performed) - When you need to stand up new API servers, start the new ones (with handling code for old operations completely unchanged) next to the old ones, and update the http-server code (like request handlers) to output the new commands. Older servers that don't understand the new commands will ignore (or redirect), and new servers that do understand will process and add to the distributed log. New nodes just stream the replications of the already existing nodes and no one spends any time with an inconsistent view of the database Of course, writing to a distributed log is slow (pick whichever consensus algo you want, you either have durability with a quorum or best-effort without), but this only is a huge deal if you're doing lots of writes, and for most web applications, that's not what's happening, the vast majority is reads. CRDTs might even fit in here, because if you want a multi-master setup, you could literally keep the log as a set (I'm not quite sure how truncation of super old records would want) keyed by transaction ID -- Assuming the same request doesn't go to multiple servers, their logs should be easily combinable at the end of the day -- API1 is gonna see events A B and E, API2 might see C and F, and API3 will likely see D G and H. Honestly everything I've described here is really more like moving the coordination/distributed log problem to the application level (up until now all this action would just happen @ the Postgres/DB level), but I'm not yet convinced it's a terrible idea. I haven't found the time to actually try to make what I'm describing here a thing but would love to hear thoughts
- SmirkingRevenge 8y agoNo need to write the distributed log yourself, you just need Kafka. You basically just independently conceived of, what is becoming a pretty popular architecture these days - event driven systems based on distributed logs. The source of truth in your system becomes the idempotent distributed log of events (rather than your rdbms or data warehouse) which ought to be "replayable", allowing you to audit or even potentially recreate your application state (databases, etc), at any point in time. Transitioning to a new version of an api/service means adding another subscriber to the event stream (and perhaps reprocessing some or all of its history), that runs in parallel with the old version - then when everything looks good you can quietly disable the old version.