Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Spring POST idempotency keys: bind replay to tenant and command

Last updated: 30 Sept 20264 min read
tutorial
IntermediateBy AITrove Editorial

A POST idempotency key identifies one accepted command within a defined ownership scope.

Download Spring source kit

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

Java
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.

spring
spring-boot
tenant-idempotency-key-scope
Storage details