bahadovic / laravel-idempotency
Production-grade, distributed idempotency and duplicate request protection for Laravel APIs.
Requires
- php: ^8.2
- illuminate/contracts: ^10.0|^11.0|^12.0|^13.0
- illuminate/database: ^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/redis: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^2.0|^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.0|^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-08 16:29:14 UTC
README
A production-grade, distributed idempotency engine for Laravel APIs. Designed for high-throughput systems, Fintech applications, and modern asynchronous Laravel runtimes (Octane, RoadRunner, Swoole).
Prevents duplicate ownership and stale-worker state commits within the idempotency state machine through atomic Compare-And-Swap (CAS) operations, lease expirations, and monotonic fencing tokens.
๐ฅ Features
- Deterministic State Machine: Requests strictly transition through
No RecordโPROCESSINGโCOMPLETED(or โCOMPLETED_NON_REPLAYABLE). - Cryptographic Request Fingerprinting: Automatically hashes Method, Path, Body, and relevant Headers using SHA-256 to reject tampered payloads with
422 Unprocessable Entity. - Atomic Concurrency: Smart
WaitStrategywith exponential backoff with jitter and microsecond-precise timeout calculation. - Stale Worker Protection (Fencing Tokens): Prevents delayed or zombie workers from committing stale responses after lease expiration.
- Octane & Long-Running Safe: Designed for long-running Laravel workers without request-scoped mutable state in the core engine.
- Native Request Scoping: Keys are logically isolated by authenticated user ID or guest IP, with support for custom Tenant/Resolver scopes.
- Safe Response Rehydration: Stores and replays actual HTTP Status Codes and Headers, whilst safely bypassing non-replayable streams.
๐ก๏ธ Architecture & Fencing
Provides atomic idempotency ownership transitions, lease-based recovery, and strict fencing protection against stale-worker database commits.
Execution & Failure Semantics
- 4xx Client Errors: By default, 4xx responses release the idempotency key so clients can fix validation/payload errors and safely retry.
- Fencing Protection on Lock Loss: If a worker loses lease ownership during execution and fails to commit or release, the engine enforces strict fencing and throws an
IdempotencyConflictException(409) to prevent returning unmanaged or stale outputs. - Manual Lease Renewal: For tasks taking longer than the default lease duration (
lock.timeout), callIdempotency::renewLease()manually to prevent lease expiration during heavy tasks.
Important Note on External Side Effects: Idempotency record fencing strictly protects database state ownership and prevents duplicate response commits. External side effects (such as third-party payment gateways, dispatching emails, or remote queues) must independently support idempotency keys or be coordinated via transactional patterns (e.g., Transactional Outbox pattern).
๐ก Observability & Events
The engine dispatches lightweight domain events through Laravel's Event Dispatcher:
IdempotencyClaimAcquired: Dispatched when a worker acquires initial execution ownership.IdempotencyResponseReplayed: Dispatched when an identical cached response is replayed.IdempotencyLockTakenOver: Dispatched when an abandoned expired lease is taken over by another worker.IdempotencyLeaseRenewed: Dispatched when an active lease is extended.IdempotencyConflicted: Dispatched when a 409 conflict occurs.
You can listen to these events in your EventServiceProvider:
use Bahadovic\Idempotency\Events\IdempotencyResponseReplayed; use Illuminate\Support\Facades\Event; Event::listen(IdempotencyResponseReplayed::class, function ($event) { logger()->info("Replayed cached response for key: {$event->key->value}"); });
๐ฆ Installation
composer require bahadovic/laravel-idempotency
Publish the configuration file and database migrations:
php artisan vendor:publish --tag="idempotency-config" php artisan vendor:publish --tag="idempotency-migrations" php artisan migrate
๐ก Usage
1. The Middleware Approach (Recommended)
Simply attach the idempotency middleware to your data-mutating routes (e.g., Checkout, Payments, Order creation):
use App\Http\Controllers\PaymentController; use Illuminate\Support\Facades\Route; Route::post('/api/payments', [PaymentController::class, 'charge']) ->middleware('idempotency');
Clients must include the Idempotency-Key header (e.g., a UUID or ULID) in their requests:
POST /api/payments HTTP/1.1 Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d Content-Type: application/json {"amount": 500}
2. The Fluent Facade Approach
For programmatic control inside complex service classes:
use Bahadovic\Idempotency\Facades\Idempotency; use Illuminate\Http\Request; public function store(Request $request) { return Idempotency::process($request->toIdempotencyRequest(), function () { // Business logic here return response()->json(['status' => 'Charged']); }); }
โ๏ธ Configuration (config/idempotency.php)
The package provides deep configurability out of the box:
header_name: Change the defaultIdempotency-Keyheader.lock.timeout: Execution lease duration (prevents deadlocks on crashes).lock.on_conflict: Decide whether concurrent requests should wait (queue up) or immediately conflict (return409).ignore_headers: A denylist of headers that should never be replayed (e.g.,Set-Cookie,Date).
๐งช Testing & Quality
This package is meticulously tested with an intense Domain-Driven Design (DDD) approach, covering state transitions, lease expiry, and concurrency fencing.
composer analyse # PHPStan Level 8 composer test # Comprehensive concurrency and edge-case test suite
๐งน Retention & Cleanup
Database Store: Run the scheduled command php artisan idempotency:prune daily to remove completed expired records and zombie rows.Redis Store: Cleanup is completely autonomous. Redis automatically expires keys using native Redis TTL (EX), requiring no cron tasks.
// In routes/console.php (Laravel 11+) or app/Console/Kernel.php (Laravel 10):
use Illuminate\Support\Facades\Schedule;
Schedule::command('idempotency:prune')->daily();
4xx Client Errors & 5xx Server Errors Handling
-
4xx Client Errors (Validation, Bad Requests):
By default, 4xx responses release the idempotency key lock immediately. Following the Stripe API standard, this allows clients to fix payload errors (e.g., correcting an invalid email or invalid amount) and safely retry the request using the same idempotency key. -
5xx Server Errors:
By default, unhandled 5xx exceptions release the lock. If you wish to cache and replay 500 errors (e.g., to prevent retrying transient gateway crashes), set'retain_5xx' => truein yourconfig/idempotency.php.
๐งฉ Compatibility
| Package | Laravel | PHP |
|---|---|---|
^1.0 |
10.x, 11.x, 12.x, 13.x |
8.2+ |
๐๏ธ Architectural Decisions & FAQ
Why not use UPDATE ... RETURNING in the Database Store?
While engines like PostgreSQL, SQLite (3.35+), and MariaDB support RETURNING clauses, MySQL (up to 8.4 LTS) does not.
To maintain 100% native compatibility across all Laravel-supported database engines without introducing brittle driver-specific query branching or raw SQL dependencies, we perform a standard conditional UPDATE followed by a single row fetch. This deliberate engineering trade-off favors maintainability, safety, and universal compatibility across the entire Laravel ecosystem.
๐ License
The MIT License (MIT). Please see License File for more information.