The "Double Click" Disaster
Imagine a user on a slow mobile network tapping the "Pay Now" button three times out of frustration. If your backend isn't prepared, you might charge their credit card three times, process three orders, and trigger a customer support nightmare. In distributed systems, network retries and duplicate requests are inevitable.
At Smart Tech Devs, when building payment gateways or critical mutation endpoints, we never assume a request will only happen once. Instead, we architect Idempotent APIs—ensuring that making a request multiple times yields the exact same result as making it once.
Implementing Idempotency in Laravel
To achieve this, we require the client to send a unique Idempotency-Key header (usually a UUID) with every POST request. We intercept this key using Laravel Middleware and Redis.
Step 1: The Idempotency Middleware
We create a middleware that checks if the system has already processed a request with the provided key. If it has, we return the cached response instead of re-running the controller logic.
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Cache;
use Illuminate\Http\Response;
class EnforceIdempotency
{
public function handle($request, Closure $next)
{
// Only apply to POST/PUT/PATCH/DELETE requests
if ($request->isMethodSafe()) return $next($request);
$key = $request->header('Idempotency-Key');
if (!$key) abort(400, 'Idempotency-Key header is required.');
$cacheKey = "idempotency:{$key}";
// If we already processed this request, return the cached response
if (Cache::has($cacheKey)) {
return response(Cache::get($cacheKey))->header('X-Idempotent-Response', 'true');
}
// Process the request normally
$response = $next($request);
// Only cache successful responses (e.g., 200, 201)
if ($response->isSuccessful()) {
Cache::put($cacheKey, $response->getContent(), now()->addHours(24));
}
return $response;
}
}
Step 2: Securing the Endpoint
Now, we simply attach this middleware to our critical routes. The client generates a UUID when they open the checkout page and sends it with the payload.
use App\Http\Controllers\PaymentController;
// Routes/api.php
Route::post('/payments/charge', [PaymentController::class, 'charge'])
->middleware(['auth:sanctum', 'idempotent']);
The Engineering ROI
By shifting idempotency handling to the middleware and caching layers, your controllers remain clean and focused on business logic. You completely eliminate the risk of race conditions, double charges, and duplicate records, creating an enterprise-grade API that clients can safely retry during network failures.
Top comments (0)