Idempotency keys, and where they have to be stored

A client that times out and retries has sent the same request twice, and the server cannot tell whether the first one succeeded. The key makes the retry safe and the storage is the whole implementation.

public function store(Request $request)
{
    $key = $request->header('Idempotency-Key');

    // the unique index does the arbitration, not the check
    try {
        $record = IdempotentRequest::create([
            'key'  => $key,
            'route' => 'orders.store',
        ]);
    } catch (QueryException $e) {
        $prior = IdempotentRequest::where('key', $key)->firstOrFail();

        return response($prior->response_body, $prior->response_status);
    }

    // ... do the work, then store the response on $record
}

Storing the response rather than just the key is what makes the second call return the same answer rather than a conflict, which is what a retrying client needs. The record has to be written in the same transaction as the work, or a crash between them leaves a key claimed and nothing done. Expiring the keys after a day or two keeps the table small and is safe, because no reasonable client retries after that long.