A POST idempotency key identifies one accepted command within a defined ownership scope.
Spring POST idempotency keys: bind replay to tenant and command
Compare the full operation before replay
The fixture stores a replay row keyed by tenant_id and request_key. Its fingerprint covers receipt ID, requested state and expected version. Sending the same key and command twice returns the stored version 2 without another state change or outbox event. Reusing the key for a different state or another receipt returns 409. The second request does not become a new mutation merely because the first succeeded.
The comparison happens before the versioned write. If it happened afterward, a legitimate retry could fail its own expected-version check. The earlier POST lesson checks local replay without this tenant-scoped command transaction. The combined command commits the replay row with state and event.
Choose retention and concurrency rules
The request key is required and bounded to a short ASCII token in this fixture. A production API needs a documented retention period, replay response contract and conflict policy. A unique database key must arbitrate concurrent first requests; this suite exercises sequential requests only. Also decide what happens when a key is reused after expiry. A fingerprint is an equality check, not a secret or an authorization decision.
Checked source
String fingerprint = receiptId + ":" + nextState + ":" + expectedVersion;
List<StoredCommand> prior = jdbc.query(
"select fingerprint, response_version from command_dedupe "
+ "where tenant_id = ? and request_key = ?",
(row, index) -> new StoredCommand(row.getString(1), row.getInt(2)),
trustedTenant, requestKey);
if (!prior.isEmpty()) {
StoredCommand saved = prior.getFirst();
if (!saved.fingerprint().equals(fingerprint))
throw new ResponseStatusException(HttpStatus.CONFLICT);
return saved.responseVersion();
}Verification boundary
TenantReceiptCommandFlowTest.replayReturnsStoredVersionWithoutSecondMutation, TenantReceiptCommandFlowTest.reusedKeyWithDifferentCommandIsConflict and TenantReceiptCommandFlowTest.sameKeyCannotTargetAnotherReceipt in the downloadable Spring source kit. The excerpt is shortened; the kit contains the complete test.
Costs and limits
Only sequential local replay is tested. There is no concurrent request barrier, expiration job, response-body persistence or target-engine uniqueness test. The fixture's fingerprint uses validated short identifiers; a real API should canonicalize every behavior-affecting field before storing its digest.
Common Mistakes
- Do not key replay by request key alone across tenants.
- Do not ignore the receipt ID when comparing commands.
- Do not run the version update before checking a legitimate replay.
Read next
Spring tenant command transaction: keep state, event and replay record together, Spring POST idempotency keys: replay the same receipt result, Spring retry and stale-write checks: choose the right HTTP contract, Spring Boot tenant command API project: assemble the local write path.
Checked outbound HTTP continuation
Continue with Spring HTTP replay key: two requests, one modeled reservation.
