azaharizaman / nexus-idempotency
Domain-level idempotency: command deduplication and replay-safe results (Layer 1).
v0.1.0-alpha1
2026-05-05 02:28 UTC
Requires
- php: ^8.3
Requires (Dev)
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-05-05 02:52:50 UTC
README
Overview
Nexus\Idempotency is a Layer 1 package for command-level idempotency: deduplicate mutating operations using a tenant-scoped composite key (tenantId, operationRef, clientKey) plus a request fingerprint, and replay a stored opaque result for safe retries.
It does not implement HTTP, databases, outbox, event streams, or audit trails—see REQUIREMENTS.md for boundaries.
Architecture
- Layer 1: pure PHP 8.3+, framework-agnostic
- Explicit lifecycle:
begin()→ domain work →complete()orfail() - Persistence via
IdempotencyStoreInterface(composite ofIdempotencyQueryInterface+IdempotencyPersistInterface; e.g.InMemoryIdempotencyStorefor tests — uses JSON-encoded tuple keysjson_encode([tenantId, operationRef, clientKey])so segments cannot collide across delimiter characters) - Time via
IdempotencyClockInterface(SystemClockreturns UTCDateTimeImmutable, or test doubles)
Key interfaces
Nexus\Idempotency\Contracts\IdempotencyServiceInterfaceNexus\Idempotency\Contracts\IdempotencyStoreInterface(extends query + persist ports)Nexus\Idempotency\Contracts\IdempotencyClockInterface
Installation
From monorepo root:
composer dump-autoload vendor/bin/phpunit -c packages/Idempotency/phpunit.xml
Usage (conceptual)
$service = new IdempotencyService($store, $store, $clock, IdempotencyPolicy::default()); $decision = $service->begin($tenantId, $operationRef, $clientKey, $fingerprint); if ($decision->outcome === BeginOutcome::Replay) { return $decision->replayResult; // replay cached outcome } if ($decision->outcome === BeginOutcome::InProgress) { // Another in-flight execution for the same key + fingerprint; surface 409 + Retry-After in Layer 3. return; } // BeginOutcome::FirstExecution — run domain command, then complete using the attempt bound to this reservation: $service->complete( $tenantId, $operationRef, $clientKey, $fingerprint, $decision->record->attemptToken, new ResultEnvelope($json), );
complete() / fail() require the AttemptToken from the FirstExecution record for that attempt so completions cannot attach to a superseded reservation after TTL expiry/replace.
License
MIT