Browse pages
Concurrency and Idempotency
Concurrency control protects invariants when accepted writes overlap.
Intent#
Concurrency control protects invariants when accepted writes overlap. Idempotency protects an operation from duplicate execution after a retry or uncertain network response.
Activation#
Activation scope: local.
Applicable specification kinds: module, business-record, use-case.
The consumer enables concurrency when accepted writes can conflict or retries can duplicate irreversible or externally visible effects.
Baseline relationship#
This extension replaces no baseline provision. litespec/ext/concurrency#return-precondition-outcomes extends litespec/backend/api#use-consistent-status-codes with the 428 row for a missing precondition.
This extension switches on limit. litespec/ext/concurrency#set-key-retention records key retention as a Limit record.
Standards#
Document write conflict behavior#
ruleguidancelitespec/ext/concurrency#document-write-conflict-behaviorA conflicting-write use case MUST document conflicting operations, invalid combined result, expected winner behavior, and user-visible conflict response.
Why
The specification identifies the invariant that version coordination protects.
Checka reviewer answers
Does the use case name conflicts, invalid state, winner behavior, and caller response?
Marked guidance: a reviewer answers this question.
Limit version checks#
rulecheckablelitespec/ext/concurrency#limit-version-checksAn implementation MUST NOT add version checks without a documented conflicting-write case.
Why
Unneeded version checks create rejection behavior without a protected business outcome.
Checkchecked by a tool
ConcurrencyVersionTests link every version check to a documented conflicting-write case.
Marked checkable: the tool named here checks it.
Compare expected aggregate versions#
rulecheckablelitespec/ext/concurrency#compare-expected-aggregate-versionsA conflicting-write implementation MUST store and compare the aggregate version through its selected persistence model.
Why
The persistence version detects a competing accepted write against the same aggregate state.
Checkchecked by a tool
ConcurrencyVersionTests race conflicting aggregate writes with expected versions.
Marked checkable: the tool named here checks it.
Map failed expected versions#
rulecheckablelitespec/ext/concurrency#map-failed-expected-versionsAn HTTP API MUST map a failed expected version to status 409 Problem Details with a stable code.
Why
A stable response lets callers distinguish a concurrency conflict from validation or authorization failure.
Checkchecked by a tool
ConcurrencyVersionTests assert a failed expected version returns 409 Problem Details with its stable code.
Marked checkable: the tool named here checks it.
Reject silent overwrites#
rulecheckablelitespec/ext/concurrency#reject-silent-overwritesA conflicting-write implementation MUST NOT silently overwrite a conflicting accepted change.
Why
Silent replacement can violate the invariant named by the use-case specification.
Checkchecked by a tool
ConcurrencyVersionTests assert conflicting accepted writes preserve the winner without silent replacement.
Marked checkable: the tool named here checks it.
Apply HTTP version preconditions#
rulecheckablelitespec/ext/concurrency#apply-http-version-preconditionsAn HTTP operation requiring a caller version MUST return a strong persistence-derived ETag and require If-Match on writes.
Why
HTTP preconditions let a caller send the version that it read.
Example
A current ETag permits the write; a changed entity version rejects it.
Checkchecked by a tool
ConcurrencyVersionTests require and validate a strong persistence-derived ETag.
Marked checkable: the tool named here checks it.
Return precondition outcomes#
rulecheckablelitespec/ext/concurrency#return-precondition-outcomesAn HTTP API MUST return 428 precondition_required for missing If-Match and 409 version_conflict for stale versions.
Why
Distinct outcomes show whether a caller omitted or supplied an obsolete version. This extends the outcome table of litespec/backend/api#use-consistent-status-codes.
Checkchecked by a tool
ConcurrencyVersionTests return 428 for a missing If-Match and 409 for a stale version.
Marked checkable: the tool named here checks it.
Hide provider version values#
rulecheckablelitespec/ext/concurrency#hide-provider-version-valuesAn HTTP API MUST NOT expose a provider-specific version value in another contract field.
Why
The ETag is the contract boundary for persistence-version coordination.
Checkchecked by a tool
ConcurrencyVersionTests assert public transport models expose no provider-specific version field.
Marked checkable: the tool named here checks it.
Scope client keys#
rulecheckablelitespec/ext/concurrency#scope-client-keysAn idempotent operation MUST scope its client key to the tenant, authenticated actor, and operation.
Why
Tenant, actor, and operation scope prevents one caller's key from replaying another operation. A project without the tenancy extension uses one fixed tenant value.
Checkchecked by a tool
IdempotencyKeyTests assert keys from different tenants, actors, or operations do not replay each other.
Marked checkable: the tool named here checks it.
Store idempotency state atomically#
rulecheckablelitespec/ext/concurrency#store-idempotency-state-atomicallyAn idempotent operation MUST store its fingerprint, state, response status, response body, and expiry with its business change in one provider transaction.
Why
One transaction connects the accepted change to the result that later calls replay. It leaves no completed record without its change and no change without its replay record.
Checkchecked by a tool
IdempotencyKeyTests commit fingerprint, state, response, and expiry with the accepted business change.
Marked checkable: the tool named here checks it.
Reject conflicting key reuse#
rulecheckablelitespec/ext/concurrency#reject-conflicting-key-reuseAn idempotent operation MUST reject reuse of one key with a different request fingerprint.
Why
One client key can represent only one accepted request intent.
Checkchecked by a tool
IdempotencyKeyTests assert reused keys with changed input return the documented rejection.
Marked checkable: the tool named here checks it.
Enforce key uniqueness#
rulecheckablelitespec/ext/concurrency#enforce-key-uniquenessAn idempotency store MUST enforce a unique constraint on tenant, actor, operation, and client key.
Why
The constraint selects one accepted winner when concurrent requests use the same key. A project without the tenancy extension uses one fixed tenant value.
Checkchecked by a tool
IdempotencyKeyTests assert concurrent inserts prove the tenant-actor-operation-key uniqueness constraint.
Marked checkable: the tool named here checks it.
Canonicalize request fingerprints#
rulecheckablelitespec/ext/concurrency#canonicalize-request-fingerprintsAn idempotency implementation MUST derive its fingerprint from canonical mapped command input, route identity, and the authenticated actor identifier, excluding credentials and the client key.
Why
Equivalent requests produce one comparison value without embedding authentication material.
The actor identifier is in the fingerprint. The credential that proved it is not in the fingerprint. Two actors can present the same client key. A fingerprint without the actor lets one actor's replay return another actor's stored response. A bearer token, a cookie, and a signature are the credential, and each one changes between two requests the rule has to treat as equal.
Checkchecked by a tool
IdempotencyKeyTests assert equivalent mapped requests produce one fingerprint without credentials or client key.
Marked checkable: the tool named here checks it.
Protect fingerprint source data#
ruleguidancelitespec/ext/concurrency#protect-fingerprint-source-dataAn idempotency implementation MUST store a cryptographic hash when support does not require original sensitive request content.
Why
A hash preserves comparison behavior while reducing retained sensitive data.
Checka reviewer answers
Is sensitive fingerprint source data hashed wherever support needs no original content?
Marked guidance: a reviewer answers this question.
Replay completed results#
rulecheckablelitespec/ext/concurrency#replay-completed-resultsA completed idempotent retry MUST return the original response status and body.
Why
A retry receives the accepted result rather than repeating its state change.
Checkchecked by a tool
IdempotencyReplayTests return the original accepted status and body.
Marked checkable: the tool named here checks it.
Define concurrent retry behavior#
rulecheckablelitespec/ext/concurrency#define-concurrent-retry-behaviorAn idempotent use case MUST define whether a concurrent request waits, reports in-progress, or returns a stable conflict.
Why
Callers need one documented result while an identical request is still active.
Checkchecked by a tool
IdempotencyReplayTests prove the documented wait, in-progress, or conflict behavior.
Marked checkable: the tool named here checks it.
Reload concurrent winners#
rulecheckablelitespec/ext/concurrency#reload-concurrent-winnersA losing concurrent idempotency request MUST reload and replay the winning record when its fingerprint matches.
Why
The unique-key winner provides the accepted result for all matching retries.
Checkchecked by a tool
IdempotencyReplayTests reload and replay the matching winner record.
Marked checkable: the tool named here checks it.
Discard failed replay state#
rulecheckablelitespec/ext/concurrency#discard-failed-replay-stateA failed pre-commit idempotent request MUST leave neither the business change nor a completed replay record.
Why
A later retry can safely perform the uncommitted operation.
Checkchecked by a tool
IdempotencyReplayTests leave no business change or completed replay state.
Marked checkable: the tool named here checks it.
Delay irreversible provider calls#
ruleguidancelitespec/ext/concurrency#delay-irreversible-provider-callsAn idempotent operation MUST NOT perform an irreversible provider call before its business transaction commits.
Why
An uncommitted operation cannot safely claim an irreversible external effect.
Checka reviewer answers
Does every irreversible provider call run only after its business transaction commits?
Marked guidance: a reviewer answers this question.
Route required external effects durably#
ruleguidancelitespec/ext/concurrency#route-required-external-effects-durablyAn idempotent operation MUST use the outbox extension or a documented provider idempotency contract for required external effects.
Why
Durable routing or provider replay protection aligns external effects with retries.
Checka reviewer answers
Does each required effect use the outbox or a documented provider idempotency contract?
Marked guidance: a reviewer answers this question.
Set key retention#
ruleguidancelitespec/ext/concurrency#set-key-retentionAn idempotency owner MUST record key retention as a Limit record set from the caller retry window and business risk.
Why
Retention lasts through the period in which callers can safely retry.
Checka reviewer answers
Does the Limit record cover the caller retry window and the business risk?
Marked guidance: a reviewer answers this question.
Bound expired-record cleanup#
rulecheckablelitespec/ext/concurrency#bound-expired-record-cleanupAn idempotency owner MUST remove expired records through a bounded maintenance process.
Why
Bounded maintenance prevents cleanup from overwhelming normal application work.
Checkchecked by a tool
IdempotencyRetentionTests show bounded deletion of expired idempotency records.
Marked checkable: the tool named here checks it.
Retain safe retry keys#
rulecheckablelitespec/ext/concurrency#retain-safe-retry-keysAn idempotency owner MUST NOT expire a key before callers can safely retry.
Why
Early expiration can turn a retry into duplicate business work.
Checkchecked by a tool
IdempotencyRetentionTests reject expiration before the documented safe retry time.
Marked checkable: the tool named here checks it.
Declare replayed transport outcomes#
rulecheckablelitespec/ext/concurrency#declare-replayed-transport-outcomesAn idempotent use case MUST list the statuses and headers stored for replay.
Why
The list defines the transport result that a completed retry can reproduce.
Checkchecked by a tool
IdempotencyOutcomeTests match replayed statuses and headers against the declared list.
Marked checkable: the tool named here checks it.
Restrict replay data#
ruleguidancelitespec/ext/concurrency#restrict-replay-dataAn idempotent implementation MUST store only safe response fields required to reproduce an accepted result.
Why
Replay data needs enough information for callers without retaining unnecessary sensitive content.
Checka reviewer answers
Does each stored replay field serve the accepted result callers reproduce?
Marked guidance: a reviewer answers this question.
Exclude unsafe replay outcomes#
rulecheckablelitespec/ext/concurrency#exclude-unsafe-replay-outcomesAn idempotent implementation MUST NOT replay authentication failures, transient server failures, Set-Cookie, Cache-Control, Vary, Expires, Age, hop-by-hop headers, tokens, or secrets.
Why
These values are unsafe, transient, or unrelated to the accepted operation result. The caching headers are computed for the response that produced them. Replaying them hands a later caller freshness instructions from an earlier moment. RFC 9111 makes each one a property of the exchange rather than of the result. The replayed response computes its own.
Checkchecked by a tool
IdempotencyOutcomeTests exclude authentication, transient failures, cookies, hop-by-hop headers, tokens, and secrets.
Marked checkable: the tool named here checks it.
Conventions#
Use the standard idempotency header#
conventioncheckablelitespec/ext/concurrency#use-the-standard-idempotency-headerUse Idempotency-Key for HTTP commands that accept client keys, bounded to 255 characters of visible ASCII.
ReplacementA consumer records a departure from this default as an Exception record with reason, review date, and removal condition.
Why
The standard header makes client-key behavior recognizable at the HTTP boundary. The IETF draft leaves the value opaque. A server then stores whatever a client sends as a key. A bound stops an unbounded key from becoming an unbounded store entry. A character set stops a key from carrying a control character into a log line.
Checkchecked by a tool
ConcurrencyTests assert applicable operations accept Idempotency-Key or record an Exception.
Marked checkable: the tool named here checks it.
Keep idempotency persistence in Infrastructure#
conventioncheckablelitespec/ext/concurrency#keep-idempotency-persistence-in-infrastructureKeep idempotency persistence and transaction handling in Infrastructure.
ReplacementA consumer records a departure from this default as an Exception record with reason, review date, and removal condition.
Why
Provider transactions and constraints belong outside Application and Domain.
Checkchecked by a tool
ConcurrencyTests assert idempotency persistence and transaction handling live in Infrastructure.
Marked checkable: the tool named here checks it.
Map replay responses at the API boundary#
conventioncheckablelitespec/ext/concurrency#map-replay-responses-at-the-api-boundaryKeep replay response mapping at the API boundary.
ReplacementA consumer records a departure from this default as an Exception record with reason, review date, and removal condition.
Why
HTTP statuses and headers are transport concerns.
Checkchecked by a tool
ConcurrencyTests assert replay response mapping lives at the API boundary.
Marked checkable: the tool named here checks it.
Declare applicable headers#
conventioncheckablelitespec/ext/concurrency#declare-applicable-headersDeclare the idempotency header as required on each applicable HTTP operation.
ReplacementA consumer records a departure from this default as an Exception record with reason, review date, and removal condition.
Why
Generated API contracts expose the requirement through litespec/backend/api#publish-precise-complete-schemas.
Checkchecked by a tool
ConcurrencyTests asserts generated OpenAPI declares the header for every applicable operation.
Marked checkable: the tool named here checks it.
Dependencies#
The baseline implementation requires no additional package.
Verification#
| ID | Mark | Evidence |
|---|---|---|
| litespec/ext/concurrency#document-write-conflict-behavior | guidance | Does the use case name conflicts, invalid state, winner behavior, and caller response? |
| litespec/ext/concurrency#limit-version-checks | checkable | ConcurrencyVersionTests link every version check to a documented conflicting-write case. |
| litespec/ext/concurrency#compare-expected-aggregate-versions | checkable | ConcurrencyVersionTests race conflicting aggregate writes with expected versions. |
| litespec/ext/concurrency#map-failed-expected-versions | checkable | ConcurrencyVersionTests assert a failed expected version returns 409 Problem Details with its stable code. |
| litespec/ext/concurrency#reject-silent-overwrites | checkable | ConcurrencyVersionTests assert conflicting accepted writes preserve the winner without silent replacement. |
| litespec/ext/concurrency#apply-http-version-preconditions | checkable | ConcurrencyVersionTests require and validate a strong persistence-derived ETag. |
| litespec/ext/concurrency#return-precondition-outcomes | checkable | ConcurrencyVersionTests return 428 for a missing If-Match and 409 for a stale version. |
| litespec/ext/concurrency#hide-provider-version-values | checkable | ConcurrencyVersionTests assert public transport models expose no provider-specific version field. |
| litespec/ext/concurrency#scope-client-keys | checkable | IdempotencyKeyTests assert keys from different tenants, actors, or operations do not replay each other. |
| litespec/ext/concurrency#store-idempotency-state-atomically | checkable | IdempotencyKeyTests commit fingerprint, state, response, and expiry with the accepted business change. |
| litespec/ext/concurrency#reject-conflicting-key-reuse | checkable | IdempotencyKeyTests assert reused keys with changed input return the documented rejection. |
| litespec/ext/concurrency#enforce-key-uniqueness | checkable | IdempotencyKeyTests assert concurrent inserts prove the tenant-actor-operation-key uniqueness constraint. |
| litespec/ext/concurrency#canonicalize-request-fingerprints | checkable | IdempotencyKeyTests assert equivalent mapped requests produce one fingerprint without credentials or client key. |
| litespec/ext/concurrency#protect-fingerprint-source-data | guidance | Is sensitive fingerprint source data hashed wherever support needs no original content? |
| litespec/ext/concurrency#replay-completed-results | checkable | IdempotencyReplayTests return the original accepted status and body. |
| litespec/ext/concurrency#define-concurrent-retry-behavior | checkable | IdempotencyReplayTests prove the documented wait, in-progress, or conflict behavior. |
| litespec/ext/concurrency#reload-concurrent-winners | checkable | IdempotencyReplayTests reload and replay the matching winner record. |
| litespec/ext/concurrency#discard-failed-replay-state | checkable | IdempotencyReplayTests leave no business change or completed replay state. |
| litespec/ext/concurrency#delay-irreversible-provider-calls | guidance | Does every irreversible provider call run only after its business transaction commits? |
| litespec/ext/concurrency#route-required-external-effects-durably | guidance | Does each required effect use the outbox or a documented provider idempotency contract? |
| litespec/ext/concurrency#set-key-retention | guidance | Does the Limit record cover the caller retry window and the business risk? |
| litespec/ext/concurrency#bound-expired-record-cleanup | checkable | IdempotencyRetentionTests show bounded deletion of expired idempotency records. |
| litespec/ext/concurrency#retain-safe-retry-keys | checkable | IdempotencyRetentionTests reject expiration before the documented safe retry time. |
| litespec/ext/concurrency#declare-replayed-transport-outcomes | checkable | IdempotencyOutcomeTests match replayed statuses and headers against the declared list. |
| litespec/ext/concurrency#restrict-replay-data | guidance | Does each stored replay field serve the accepted result callers reproduce? |
| litespec/ext/concurrency#exclude-unsafe-replay-outcomes | checkable | IdempotencyOutcomeTests exclude authentication, transient failures, cookies, hop-by-hop headers, tokens, and secrets. |
| litespec/ext/concurrency#use-the-standard-idempotency-header | checkable | ConcurrencyTests assert applicable operations accept Idempotency-Key or record an Exception. |
| litespec/ext/concurrency#keep-idempotency-persistence-in-infrastructure | checkable | ConcurrencyTests assert idempotency persistence and transaction handling live in Infrastructure. |
| litespec/ext/concurrency#map-replay-responses-at-the-api-boundary | checkable | ConcurrencyTests assert replay response mapping lives at the API boundary. |
| litespec/ext/concurrency#declare-applicable-headers | checkable | ConcurrencyTests asserts generated OpenAPI declares the header for every applicable operation. |