Skip to content

Idempotency

Viames Marino edited this page Feb 23, 2026 · 2 revisions

Pair framework: Idempotency

Pair\Api\Idempotency prevents duplicate execution of mutating endpoints.

Implementation details:

  • file-based storage under TEMP_PATH/idempotency
  • key scoped by custom scope string + client idempotency key
  • request hash check (method + URI + body)
  • replay of previously stored response

Required request headers

Client can send one of:

  • Idempotency-Key
  • X-Idempotency-Key

Typical usage pattern

use Pair\Api\Idempotency;
use Pair\Api\ApiResponse;

Idempotency::respondIfDuplicate($this->request, 'orders:create');

$result = ['orderId' => 123, 'saved' => true];

Idempotency::storeResponse($this->request, 'orders:create', $result, 201);
ApiResponse::respond($result, 201);

Main methods

respondIfDuplicate(Request $request, string $scope, int $ttlSeconds = 86400): bool

Behavior:

  • no key -> returns true and request continues
  • existing key with same hash and completed response -> returns cached response immediately
  • existing key with same hash and status processing -> CONFLICT
  • existing key with different request hash -> CONFLICT
  • new key -> writes processing lock

storeResponse(Request $request, string $scope, mixed $data, int $httpCode = 200, int $ttlSeconds = 86400): bool

Stores canonical response for future retries with same key.

clearProcessing(Request $request, string $scope): bool

Clears lock file, useful in explicit rollback/error flows.

Best practices

  • Use stable scope names (orders:create, billing:payInvoice).
  • Always call storeResponse(...) before sending final success response.
  • Keep TTL aligned with retry windows used by clients/offline queues.

Frequent usage recipes

Full safe flow with rollback cleanup

use Pair\Api\Idempotency;
use Pair\Api\ApiResponse;

Idempotency::respondIfDuplicate($this->request, 'orders:create', 86400);

try {
    $payload = $this->request->validate([
        'customerId' => 'required|int',
        'amount' => 'required|numeric|min:0.01',
    ]);

    $result = [
        'orderId' => 2241,
        'customerId' => (int)$payload['customerId'],
    ];

    Idempotency::storeResponse($this->request, 'orders:create', $result, 201, 86400);
    ApiResponse::respond($result, 201);
} catch (\Throwable $e) {
    Idempotency::clearProcessing($this->request, 'orders:create');
    throw $e;
}

Same key, different payload protection

If the same idempotency key is reused with a different body/URI/method hash, Pair returns CONFLICT. This protects against accidental key reuse bugs in clients.

Optional behavior without key

// no idempotency header -> method returns true and request continues normally
Idempotency::respondIfDuplicate($this->request, 'payments:create');

Common pitfalls

  • Forgetting clearProcessing() in exception paths.
  • Using generic scopes that collide across operations.
  • Storing huge response payloads unnecessarily in idempotency files.

See also: API, Request, ApiResponse, PWA.

Clone this wiki locally