truschery / idem
Idempotency for HTTP requests, queued jobs, and arbitrary operations
Requires
- php: >=8.2
- illuminate/cache: *
- illuminate/database: *
- illuminate/http: *
- illuminate/queue: *
- illuminate/support: *
- truschery/kanon: ^1.0
Requires (Dev)
- laravel/pint: ^1.29
- orchestra/testbench: ^10.11
- pestphp/pest: ^3.8
- phpstan/phpstan: ^2.1
README
Laravel Idempotency
Idempotency for HTTP requests, queued jobs, and arbitrary operations
Ensures that repeating the same operation always produces the same result with no side effects. Essential for payment systems, APIs, and any operation where duplication is unacceptable.
Features
Idempotent— HTTP middleware: a repeated request with the sameIdempotency-Keyreturns the cached responseEnsureIdempotent— Job middleware: a queued job is executed only once, even if dispatched multiple timesOnce::do()— facade for arbitrary operations: any callable runs exactly once per key- Two storage drivers:
cacheanddatabase
Installation
composer require truschery/idem
Publish the configuration file:
php artisan vendor:publish --tag=idem-config
If you plan to use the database driver, publish and run the migrations:
php artisan vendor:publish --tag=idem-migrations php artisan migrate
Usage
HTTP Requests — Idempotent
Register the middleware alias and apply it to a route or group:
// Routes Route::post('/payments', [PaymentController::class, 'store']) ->middleware('idempotent');
The client sends a unique key in the request header:
POST /payments HTTP/1.1 Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 Content-Type: application/json {"amount": 1000, "currency": "USD"}
A repeated request with the same key returns the stored response without re-executing the handler.
If the
Idempotency-Keyheader is absent, the request is processed normally — no error is thrown.
Queued Jobs — EnsureIdempotent
Pass the idempotency key directly to the middleware constructor:
use Truschery\Idem\Middleware\EnsureIdempotent; class ProcessPaymentJob implements ShouldQueue { public function __construct(private string $paymentId) {} public function middleware(): array { return [new EnsureIdempotent($this->paymentId)]; } public function handle(): void { // Runs only once, even if the job is dispatched multiple times Payment::process($this->paymentId); } }
Arbitrary Operations — Once::do()
use Truschery\Idem\Once; $result = Once::do('send-welcome-email:' . $user->id, function () use ($user) { return Mail::to($user)->send(new WelcomeMail($user)); });
A repeated call with the same key returns the cached result of the first execution.
Once::do() works in any context — HTTP requests, queued jobs, Artisan commands.
Roadmap
- HTTP Middleware (
Idempotent) - Job Middleware (
EnsureIdempotent) -
Once::do()facade -
cacheanddatabasedrivers - Extended tests for Job middleware and
Once::do() - Artisan command
idem:prune— removes expired records from the database
License
MIT — see LICENSE for details.