DEV Community

Cover image for Prevent Double Charges: Idempotent APIs in Laravel 🛡️
Prajapati Paresh
Prajapati Paresh

Posted on • Originally published at smarttechdevs.in

Prevent Double Charges: Idempotent APIs in Laravel 🛡️

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)