Two requests, one intention
Imagine a customer buying a desk lamp. The first request reaches the order service, which inserts order o481 and commits. The server sends a response, but it never reaches the phone. After five seconds, the phone retries. Another server receives the second request, sees a perfectly normal create operation, and inserts o482.
12:00:00 Client sends checkout c781
12:00:01 Server commits order o481
12:00:01 Response gets lost
12:00:05 Client times out and retries
12:00:06 Another server commits order o482
Nothing in that sequence requires two clicks. A timeout means the caller did not receive an answer before its deadline. It does not prove that the server failed. TCP retransmission is not the issue either. The client has sent a second application request after losing the outcome of the first.
Before discussing Redis locks or database isolation levels, I would state the rule we need to protect: one checkout operation may create at most one order for a given tenant. A customer can still buy two identical lamps intentionally. Same request data does not necessarily mean same intention.
Make the operation recognizable
The client should create an operation ID when the checkout starts, before the first network attempt. Every retry carries that same ID. A genuinely new checkout gets a different ID. In our example, that ID is c781.
It helps to distinguish three identities. A trace ID identifies one request attempt for observability. The checkout ID identifies one business operation across attempts. The order ID identifies the resulting resource. One checkout might involve three HTTP requests with three trace IDs, but it should still resolve to one order ID.
The server scopes c781 to the authenticated tenant and operation type. It also keeps the meaningful request parameters, such as cart version, currency, and quoted amount, or a fingerprint of those parameters. If the same ID arrives with a different basket, that is not a harmless retry. Return a conflict rather than quietly changing the accepted order.
The database must resolve the race
A common implementation first queries for c781, then inserts a new order when nothing is found. That is not safe. Two application instances can both query before either inserts. Both then decide to create an order.
I would store the operation identity alongside the order in the same relational database, with a unique constraint on (tenant_id, operation_type, operation_id) and all three identity columns declared NOT NULL. In one short transaction, claim that identity, create the order and its items, and record the original creation result. If another request already owns the identity, retrieve its recorded outcome after the competing transaction resolves.
BEGIN
Claim unique checkout identity c781
If new, create o481 and record its acceptance result
COMMIT
On a retry, resolve c781 to o481
This is a sketch of the transaction boundary, not ready-to-run SQL. The database constraint settles concurrent attempts, not an application-side check and not a process-local mutex. The order and its operation identity either commit together or neither commits.
Now stop the server at different times. If it crashes before commit, a retry can create the order. If it crashes immediately after commit, the retry finds o481. If it crashes after responding, another retry still finds o481. In all three cases, the result is consistent with the stored decision.
There are two easily missed details. A competing transaction may still be in flight, so a retry may need to wait. If the caller's deadline expires during that wait, keep the same operation ID and report an unresolved outcome rather than inventing another checkout. Also, returning an existing result still requires authorization. Knowing an operation ID must not be enough to see someone else's order.
What does the client show?
Suppose the first request timed out and the app displays “Order failed. Try buying again.” That wording invites the customer to start a new operation even though the first may have committed. The API may be correct while the product still creates duplicates.
I would preserve the pending checkout identity across retries and display something like “Checking your order” until the result is known. The retry returns the original acceptance result and order ID. A separate order endpoint can provide its current fulfillment state, which may have progressed since the first response.
There is also a limit to the guarantee. If the operation record is deleted after a day and the same identity can be accepted again, that later request may be treated as new. Retention therefore belongs in the API contract. You cannot promise lifetime deduplication while forgetting the identity.
In an interview, I would finish by distinguishing the operation identity from the database enforcement. A key in the request alone does not solve concurrent creates. The unique operation claim and the business mutation need one atomic boundary, and the client must preserve that identity when the response is unknown.
The order is now created once. But the warehouse does not read the order database. It waits for an event. What happens when the order commits and the application dies before publishing it?