64

This is kind of similar to the Two Generals' Problem, but not quite. I think there is a name for it, but I just can't remember it right now.

I am working on my website's payment flow.

Scenario

  1. Alice wants to pay Bob for a service. Bob has quoted her US$10.
  2. Alice clicks pay.
  3. Whilst Alice's request is flying through the ether, Bob edits his quote. He now wants US$20.
  4. Bob's request finishes before Alice's has reached the server.
  5. Alice's request reaches the server and her payment is authorized for US$20 instead of US$10.
  6. Alice is unhappy.

Whilst the chances of this are very low in practice, it is a possible scenario. Sometimes requests can hang due to network issues, etc...

Possible Mitigations

I don't think this problem is solvable. But we can do things to mitigate it.

This is not exactly an idempotency issue, so I don't think the answer is "idempotency token".

Option 1

Let's define:

  • t_0 as the time Alice click pay.
  • t_edit as the time Bob's edit request succeeds
  • t_1 as the time Alice's request reaches the server

Since we cannot know t_0 unless we send it as part of the request data, and because we cannot trust what the client sends, we will ignore t_0.

At the time Alice's request arrives in the server, we check:

if t_1 - t_edit < 1 minute: return "409 Conflict" (or some other code)

Would this approach work? 1 minute is an arbitrary choice, and it doesn't solve the problem entirely. If Alice's request takes 1 minute or more to reach the server, the issue persists.

This must be an extremely common problem to deal with, right?

turnip
  • 1,701

10 Answers10

152
  • Alice wants to pay Bob for a service. Bob has quoted her $10.

Give this quote a unique token.

  • Alice clicks pay.

When this response is send to the server, it must go with the token of what is being paid. This also allows you to discard duplicate payments.

  • Whilst Alice's request is flying through the ether, Bob edits his quote. He now wants $20.
  • Bob's request finishes before Alice's has reached the server.

That has a new different token※. The server must invalidate the old one.

  • Alice's request reaches the server and her payment is authorized for $20 instead of $10.

No, it isn't. Alice token does not match.

※: The server must send Alice the new quote, with the new token. And alice must click pay again.


For user experience, you can also add a timeout. That prevents the token to be used right away. This timeout can either be only client side or networked. The purpose is to give some time to the user to notice the change.

This must be an extremely common problem to deal with, no?

Many online video games that allow players to trade face this problem. A simple 5 seconds timeout can save support a lot of headaches.

Theraot
  • 9,261
58

A quote should be a write-once record.

Bob isn't allowed to edit it once it has been created and passed to Alice.

You can ensure this at different levels, from simply not offering an edit dialog to sophisticated digital signature algorithms.

Bernhard Barker
  • 548
  • 3
  • 8
29

This must be an extremely common problem to deal with, no?

No, it isn't. I doubt you'll be able to find a payment processor that lets you change the amount after the customer has authorised a particular amount.

You sell to Alice at the price she authorised, because that's what your quote to her was, and what she authorised. You don't check that the money you received matches what you currently quote. If you do check, it's that you issued that quote to Alice in the first place.

Caleth
  • 12,190
20

Just send the amount Alice agreed to pay along with the request. If the price has increased since Alice sent the request, you send a response indicating that the item could not be purchased at or below that price, and the current price is whatever it is. This is pretty much the same situation as when there's only one item available and, at the time the request to buy reaches you, the item has already been sold.

So your message flow would look like:

   Bob → Server  Set item price to $10
 Alice → Server  Tell me item price.
Server → Alice   Item price is $10.
   Bob → Server  Set item price to $20.
 Alice → Server  Purchase item for $10.
Server → Alice   Item not available for $10; current price is $20.

This is, in fact, exactly how things work in trading systems connected to exchanges. (I wrote one about ten years ago.) You never know what bids and offers are currently on the exchange; you know only what was there a few milliseconds ago. And even if what was there is still there now, it may not still be there when your order reaches the exchange.

cjs
  • 783
8

Yes this is a common issue, and it is about transactional consistency.

To summarize your issues:

  • the quote is binding for the seller. In general it has a reference and an expiration date/time.
  • the buyer may buy under the condition of an accepted quote. The buyer cannot be forced to accept a price that was not agreed.
  • if a buyer finishes the transaction before the expiration of the quote, there is no ambiguity about which price to use. But if you allow the seller to change the quote, either the price should be the one that the buyer accepted when starting the purchase, or the user should give consent to the price of the new quote.
  • if the buying transaction is started before the price change, but not finished in time, we are in an ambiguous situation in which the seller could decide not to accept the purchase under the old price; but the buyer is not obliged to buy under the new price. If we are speaking of minutes and seconds, the practice is often to accept the initiated transaction.
  • finally, there might be no explicit quote for a buyer, in the scenario of a public price list for a catalogue.
  • the main problem in your specific situation, is that you have no certainty about the time at which Alice initiated the request: you only have a time for the completion of the payment.

One solution that works for both, quotes and public price lists, is to give a certainty to the start time of the transaction. So before proceeding to the payment, Alice must confirm her intention to buy. Exactly like with your Amazon basket. If at this moment the price was already changed, Alice could decide to accept and continue, or to cancel the purchase.

Since you now know the purchase initiation date, you can fine-tune your business rules (e.g. accepting a payment within 10 minutes, or more, or less).

From a technical point of view, this approach can be used to implement the saga pattern that fully solves the issue:

  • preparatory actions (request a quote, issue a quote, consult a quote) that are all cancellable, until a pivot event;
  • all actions happening after the pivot are just optimistically performed as if we’d be sure of a positive outcome;
  • but the these actions must stay reversible (status "in progress", with undo possibility) until the last event of the saga is performed (payment completed in your case).

The saga is a more flexible alternative to a distributed two phase commit.

And if Alice and Bob were on the same system, there wouldn’t be an issue at all thanks to ACID isolation.

Christophe
  • 81,699
4

Bob changed the price at time t. Alice ordered at a time t’ which is close to t. Bob would have had no problem to charge the lower price, had she ordered 10 minutes away from t.

So you record not only the current price, but also the previous price and when it was changed. In Alice’s order you include the price she has seen.

When the order arrives and doesn’t match the current price: If it doesn’t match the previous price either, you fail (some dodgy request). If the price increased, but more than ten minutes ago, you fail. Otherwise, that is the price was lowered or changed in the last ten minutes, you charge the lower of current and previous price.

All this of course if Bob agrees. The “ten minutes” can be made longer. Easy to implement, and it keeps customers happy. If Bob prefers to make customers unhappy, or never return to the shop, you implement something else.

To make the situation less common, let Bob edit the prices at any time, but apply all the changes at 3am in the night when (almost) nobody is ordering.

gnasher729
  • 49,096
2

In an API world, this would be solved by resource versioning. The resource can be versioned internally, for example with a time stamp.

For a standardized mechanism, an ETag can be used.

This allows for semantics such as "the thing we talked about earlier, I'd like to perform an operation on it, if things are still the same"

Martin K
  • 2,947
1

While Theraot's solution of associating each price offer from Bob with a unique token works (and may have some additional benefits like preventing Alice from accidentally paying for the same service twice, assuming that's not something she'd normally ever want to do), for this particular problem there's an even simpler solution:

Include the price that Alice is willing to pay in her purchase request.

(Edit: This is essentially the same solution as suggested by Curt J. Sampson earlier. Somehow I failed to notice their answer before writing mine. I'll leave this answer here since it includes some additional details, but I encourage you to upvote their answer too if you like this one.)

With that single modification, your example scenario now works out like this (with changes in italics):

  1. Alice wants to pay Bob for a service. Bob has quoted her US$10.
  2. Alice clicks pay, sending a request to purchase Bob's service for US$10 to the server.
  3. Whilst Alice's request is flying through the ether, Bob edits his quote. He now wants US$20.
  4. Bob's request finishes before Alice's has reached the server.
  5. Alice's request reaches the server and is rejected, since the price of US$10 that Alice is offering to pay does not match the US$20 that Bob is now asking.
  6. Alice receives a message that her purchase failed due to a price mismatch, and she must now choose between repeating the purchase with the new price of US$20 or rejecting the new offer. Alice is mildly annoyed at Bob for suddenly switching prices like that.

Note that, since the price of US$10 included in Alice's request in step 2 comes from Alice's browser, which is under her control, she could fairly easily modify the request to try and get a cheaper price. However, the server-side comparison of the prices in step 5 will also protect you and Bob against any such attacks by Alice: any attempt by Alice to unilaterally lower the price she's paying will just give her the same notice of a price mismatch and force her to retry the purchase, just as if Bob had changed the price.

If you want, you can try to distinguish these two scenarios, e.g. by keeping track of recent price changes by Bob on the server and/or by using a cryptographic token passed from the server to Alice and back to verify that Alice's request indeed matches a legitimate prior offer by Bob. This could be useful if you wanted to know whether Alice was trying to cheat or just a victim of unfortunate circumstances, but it's not needed to prevent such cheating attempts from working in the first place.


Also note that, if Bob had decided to instead lower his price from US$10 to e.g. US$5, you would have several options for handling the mismatch:

  • a) reject the request and make Alice repeat it, just like above;
  • b) accept Alice's request, but only charge her the new price of US$5, just as if she had repeated the request and accepted the new price; or
  • c) accept Alice's request and make her pay the original price of US$10, just as if Bob's price change had only happened after Alice's purchase.

In some sense, none of these options is wrong — they all (eventually) result in Alice paying a price that both she and Bob had considered acceptable for the service. That said, going with option (c) seems likely to leave Alice quite unhappy if she realizes what has happened. Thus, in general, I'd recommend either option (b) or, just possibly, some low-overhead version of (a) where Alice is only shown a quick confirmation dialog where she can click "OK" to accept the new, lowered price. Anything more than that would be needless overhead for something that Alice almost surely does want to do.

Of course, any such confirmation request must also include the new price that Alice now wants to pay, and it must be verified against Bob's offer on the server in order to protect against further price changes by Bob and/or attempts to manipulate the request by Alice.


BTW, unless your app includes a real-time feed of price changes from the server to each potential customer's browser, a much more likely version of your scenario is that Bob changes the price after Alice has loaded the page with the price and the purchase button, but before she has actually clicked the button. That's typically a much wider time window than the actual time it takes from Alice's request to reach the server after she clicks the purchase button. However, it doesn't actually make any practical difference for this scenario whether the price change occurs before or after the button click — in general, neither Alice nor Bob nor the server can even tell anything except that the price has changed at some point after Alice loaded the page and before her request reached the server.

(If your app does include a real-time price change feed, you'll need to also consider the possibility of Bob changing the price — and this price change reaching Alice's browser — a fraction of a second before Alice clicks the button, too late for her brain to react to the change and stop the click. It would probably be a good idea to disable the purchase button for at least a few seconds after any price change, and to show some very conspicuous notification whenever such a live price change occurs.)

1

I would like to address two side issues. How to solve the issue has been addressed by many others.

Criticality of the issue

You missed steps 7 through 9 in the scenario:

  1. Alice wants to pay Bob for a service. Bob has quoted her US$10.
  2. Alice clicks pay.
  3. Whilst Alice's request is flying through the ether, Bob edits his quote. He now wants US$20.
  4. Bob's request finishes before Alice's has reached the server.
  5. Alice's request reaches the server and her payment is authorized for US$20 instead of US$10.
  6. Alice is unhappy.
  1. Alice talks to her credit card company, tells them she agreed to pay US$10, and they agree.

  2. Bob gets his US$20.

  3. You pay the remaining US$10. Now you are unhappy.

This makes it critical that you deal with it. Otherwise, some Alice and Bob will collude to take money from you.

Commonness of the issue

If Alice performs step one on Monday morning, and step two on Friday night, step three could have happened any time in between. This makes it ridiculously easy to happen.

David G.
  • 264
  • 1
  • 2
  • 4
0

Note that in most cases there's an intermediate step--shopping carts. You put the item in the shopping cart rather than buying it. If there's a price change at the wrong instant you can see it before you press pay. When you pay you're buying the shopping cart--the items in it already have prices attached.

For a more complete solution you can make making prices read-only. When Bob raised the price on widgets to $20 he really created a new type of widget that goes for $20, the $10 widget still exists but can not be found unless you know the item ID. Alice is attempting to purchase the $10 version, the transaction goes through at $10.