mikaeledstrom's avatar

eCommerce: how to avoid double order submission

Hello!

When our e-commerce customers arrive to our order confirmation page (upon we create customer object, order rows etc) from our payment provider, sometimes (perhaps 1 every 250 orders) it happens twice at the exact time.

The implication is that we will create two orders, register two payments, sometimes create two users.

When browsing through the Laravel app logs, we see that the two requests happen at the exact same time.

How can this be solved?

Can we use Redis cache to have a list (basically an array) with the latest "payment provider order ids" and do a lookup early on the confirmation page controller:

if payment provider order id is in list, abort else create order etc

How would you solve this?

0 likes
11 replies
Snapey's avatar

first thing to try is disable any user input button as soon as they click it.

Or did I misunderstand the question?

Next step would be to make sure you are updating the status of the order in an atomic fashion

mikaeledstrom's avatar

@Snapey We don't controll/own the UI before the redirect to our confirmation page so your first suggestion (disable user input button) is not possible.

The other solution is exactly what I want to learn more about. Isn't that somewhat what I write about with a cache using atomic lock?

Snapey's avatar

you could use redis, but unless you are careful, the problem will be the same.

  • thread #1. orderref in array? No.
  • thread #2. orderref in array? No.
  • thread #2. add order ref to array.
  • thread #1. add order ref to array

I'm sure you are aware of this already

You can use Laravel's atomic locks to prevent another thread creating a duplicate order, but in your code before you create an order you will need to check if the order already exists. If you put the lock around this initial step then this will prevent two threads doing it at the same moment. Put the minimum code inside the lock.

Cache::lock('addOrder')->get(function () {
    // Lock acquired indefinitely and automatically released...

    // Is there an order in existence
    // yes, exit
    // no? create order
});

// rest of your code
mikaeledstrom's avatar

@Snapey Cache::lock is what I am learning more about, thank you. a) would you use the block method to trigger an exception? b) the code inside the closure, would you use Cache (e.g. Redis) for the check if there is an order in existance or use database (e.g. MySQL)?

Snapey's avatar

@mikaeledstrom you should be able to use whatever you use to store the order

just check if there is already a db record with the same order id, and if there is, don't save and report it somehow

martinbean's avatar

@mikaeledstrom Payment gateways will send some form of transaction ID or idempotency key to avoid exactly this.

Which payment gateway is this?

mikaeledstrom's avatar

@martinbean it is a local payment provider. They send a unique order ID, basically a transaction ID. That is how we can find these occurrences, by checking for payments with the same transaction ID. It would be much nicer to avoid it completely.

martinbean's avatar

@mikaeledstrom So therefore I don’t see the problem? I mean, first off the column in the database should be unique to make it literally impossible for two rows to have the same transaction ID.

It’s also impossible for two requests—and SQL INSERT statements—to happen at the exact same nanosecond. A unique key on the column you store the transaction ID will prevent the second request.

To further ensure an order is processed only once and exactly once, you can push the order creation to a queue. Queued jobs will be executed sequentially, no matter what order or time they were added to the queue. One queue job will create the order. If another queue job thereafter then attempts to write an order with the same ID, then you’ll get an exception because of the unique key constraint violation.

I’d like to give you more concrete answer, but given we’ve not seen any code and your reluctance to name the payment gateway as if it were some national secret, I can’t consult any documentation and say for sure how the gateway should be used and duplicates prevented.

mikaeledstrom's avatar

@martinbean

The payment provider is not some national secret at all, their name is Svea Ekonomi.

I made the transaction id column unique. With that, I can catch the query error if duplicate. If code is 1062, then we have a duplicate error.

If I make a bash script that uses CURL's parallel function, all requests without one will become "duplicate". However, it is not the first in the log file that is "duplicate = false", isn't that a bit strange?

Please or to participate in this conversation.