# Design durable idempotency for retryable mutations

Reason about and specify durable idempotency protocols for ambiguous retries, including request canonicalization and schema evolution.

Exact reference: {"kind":"skill_version","skill_id":"skl_NHXa-LFii9L5hQ2p_eFutA","version_id":"skv_Kx_nThwWig_iyBrr8sAHAQ"}

Applicability: [{"constraint":"The idempotency record and local business effect can commit in one atomic transaction.","technology":"transactional database","version_scheme":"unknown"},{"constraint":"Legacy canonicalizers or explicit lossless compatibility bridges remain available for the idempotency retention period.","technology":"versioned request schema","version_scheme":"unknown"}]

# Durable idempotency for retryable mutations

Use this pattern when a client can retry after a timeout and the first attempt may already have committed, including when request schemas or canonicalization rules change during the retry window.

## Required invariant

For one idempotency scope and key, at most one business effect may commit during the key retention period.

The uniqueness namespace must remain stable across request and fingerprint versions. Never include the fingerprint version in the unique key, because that would permit one committed effect per version.

## Minimal durable state

Persist a record with:

- An idempotency scope, such as tenant and operation.
- A client-generated idempotency key reused for retries of the same logical request.
- The request contract version.
- An immutable fingerprint algorithm version.
- The request fingerprint digest.
- A replayable result, either the original response or an immutable semantic result.
- The stored response or result schema version.

Enforce a database unique constraint on scope plus operation plus key.

Legacy records without explicit version columns must be assigned a fixed legacy version by migration or read logic. Do not infer their version from the currently deployed canonicalizer or from wall-clock time.

## Transaction protocol

1. Begin one database transaction.
2. Attempt to insert the idempotency record for the scope and key.
3. If a committed record exists, compare the incoming request under the record-directed rules below. Replay on a verified match and reject key reuse on a mismatch.
4. If the insert succeeds, apply the business mutation in the same transaction.
5. Store the replayable outcome and its response or result version.
6. Commit once, then return the outcome.

An uncommitted unique-key insert may cause a concurrent retry to wait. After the first transaction resolves, the retry either observes the committed record and compares against it, or acquires the key after rollback and safely performs the mutation.

## Versioned fingerprint comparison

Fingerprint algorithm identifiers are immutable. Changing parsing, defaulting, field inclusion, number handling, Unicode handling, or canonical serialization creates a new identifier.

The client may identify its request contract version, but it does not choose the fingerprint algorithm used for an existing key. The stored record selects that algorithm.

For a same-version retry:

1. Validate against the closed request schema for that version. Reject unknown fields rather than silently ignoring them.
2. Run the retained canonicalizer named by the record.
3. Compare the resulting digest with the stored digest.

For a cross-version retry:

1. Parse the incoming request under its declared contract.
2. Use an explicitly reviewed directional bridge from the incoming contract to the stored contract.
3. Reject if any effect-bearing field is unknown, unmapped, or would be discarded.
4. Convert the request to the stored contract and round-trip it back to the incoming contract.
5. Require semantic equality of every effect-bearing field after the round trip. Explicitly documented neutral defaults may compare equal.
6. Only after that proof, canonicalize with the stored fingerprint algorithm and compare its digest.

Never fall back to feeding a new-schema body directly into a legacy canonicalizer. A legacy canonicalizer may ignore a field that has become meaningful.

If a legacy digest was produced by a lossy canonicalizer and neither the original normalized request nor an immutable business intent was retained, the lost distinction cannot be reconstructed. Mark that version pair incompatible and reject cross-version key reuse. Do not guess.

## Compatible response replay

Request identity and response representation are separate decisions.

- An old client can receive the exact stored status, headers where appropriate, and body.
- A newer client may receive a new response representation only through an approved adapter from an immutable stored result.
- If no safe response adapter exists, return the original representation when the client accepts it or report an incompatible replay. Never rerun the mutation to manufacture a new response.
- Do not reconstruct the original outcome from mutable current resource state.

## Overlapping retries

When compatible old-contract and new-contract requests use the same key concurrently, the database unique constraint still selects one winner. After the winner commits, each loser compares against that exact record. Compatible requests replay its result; incompatible requests receive a conflict and do not execute.

If conflicting requests race before any record has committed, idempotency guarantees at most one effect but cannot determine which conflicting intent the caller preferred. The uniqueness winner binds the key, and every incompatible loser must be rejected.

## Safe rollout order

1. Expand storage to carry request version, fingerprint version, and result version. Treat existing rows as one fixed legacy version.
2. Deploy readers that understand all versions that writers may create. A reader that encounters an unknown version must fail closed without executing the mutation.
3. Retain legacy parsers and canonicalizers, or safe bridges, for at least the idempotency retention window.
4. Switch writers to the new fingerprint version.
5. Retire legacy logic only after no retained record can require it, or after a verified migration based on retained normalized request data. A digest alone cannot be translated safely into a new digest.

## Failure reasoning

- Crash before commit: the idempotency record and business mutation both roll back. A retry may execute once.
- Commit followed by a lost response: the record and mutation both exist. A retry verifies against the record version and replays without executing.
- Concurrent identical requests: the unique constraint serializes ownership; only the winner executes.
- Same key with different content: fingerprint comparison or the cross-version compatibility gate rejects the request.
- Unknown record version: fail closed; do not reinterpret the digest with current code.

## Boundaries

This is not exactly-once message delivery. It provides at most one committed business effect per key for as long as the durable record or an equivalent business uniqueness constraint is retained. Retention must exceed every legitimate retry horizon.

Do not rely on memory caches or a non-atomic check followed by mutation. Do not commit an in-progress marker separately unless the design also specifies leases and abandoned-work recovery.

For an external effect that cannot join the database transaction, atomically write an outbox entry with the mutation. Deliver it with a stable key, and require a downstream uniqueness constraint or idempotent operation.

## Supporting basis and limitations

Derived from atomic transaction boundaries, database uniqueness behavior, crash recovery states, concurrent retry races, information loss across schema projections, and response-version compatibility.

## Change and rationale

Extends the design with immutable fingerprint versions, lossless cross-version compatibility checks, versioned response replay, and safe rolling-deployment behavior.

Idempotency records commonly outlive request-schema and canonicalization deployments. Without a record-directed version and a reject-by-default bridge, a newer request can falsely match an older key when legacy normalization drops newly meaningful information.
