GraphQL / Advanced: bounded and distributed execution / Lesson 14
Idempotent writes, conflicts and transactions
Protect business effects across retries with a deliberate idempotency and revision contract.
On this page
Lesson goals
- Separate request identity, object identity and optimistic revision.
- Reason about duplicate requests and atomic domain boundaries.
Design around a lost response after commit
A mutation can commit successfully while its response is lost. A blind retry may then repeat an external effect or return a conflict that hides the first success. An idempotency contract associates a stable request key with a principal, operation and canonical input fingerprint, then returns the same recorded outcome for a legitimate replay. Object ID and operation name alone are insufficient because one object can receive many distinct edits. Key scope, retention, concurrent duplicate handling and recovery must be explicit in the domain implementation.
Keep idempotency separate from stale-write detection
An expectedRevision detects that another accepted edit changed the object after the client read it. A requestKey identifies this particular logical attempt across transport retries. They solve different problems and may appear together. The input below is a schema design example, not an implementation of atomic storage. A reused key with different input should be rejected under a stable conflict policy. Validate ownership and permitted state transitions before the write, and avoid returning another principal’s recorded outcome if an attacker guesses a request key.
input PublishCourseInput {
id: ID!
expectedRevision: Int!
requestKey: String!
}
type PublishCoursePayload {
course: Course
problems: [Problem!]!
}
expectedRevision protects a read-modify-write precondition; requestKey supports replay recognition. The domain service must implement both atomically where required.
Record the effect and replay outcome coherently
Checking for a request key and then writing in separate unprotected steps allows concurrent duplicates to pass the check. The durable change and idempotency outcome need a coherent storage protocol, commonly a transaction with uniqueness constraints appropriate to the chosen source. External effects cannot automatically join that transaction. A transactional outbox can record intended delivery alongside the change, while delivery and consumers still require retry and deduplication rules. GraphQL supplies a mutation entry point; it does not make these distributed consistency guarantees on behalf of the application.
Model replay identity without a storage claim
This synchronous in-memory model increments a synthetic revision once and returns the same result for an identical replay. Reusing the key with a different fingerprint raises a conflict. The input fingerprint is supplied as a literal for clarity; production canonicalization needs an explicit format and safe handling of sensitive input. The model has no parallelism, persistence or crash recovery, so it does not establish a production idempotency protocol. Its purpose is to make the distinction between a new business edit and retransmission visible in a small test.
const seen = new Map();
let revision = 4;
function publish(key, fingerprint) {
if (seen.has(key)) {
const old = seen.get(key);
if (old.fingerprint !== fingerprint) throw new Error("KEY_REUSED");
return old.result;
}
const result = {revision: ++revision};
seen.set(key, {fingerprint, result});
return result;
}
console.assert(publish("r1", "c1@4").revision === 5);
console.assert(publish("r1", "c1@4").revision === 5);
console.assert(revision === 5);
The identical replay does not increment again in this single-process fixture. Concurrent and crash-safe behavior remains unimplemented.
Test the uncertain and conflicting paths
A write test matrix should include first success, identical replay, key reused with different input, concurrent duplicate, stale revision and response loss after commit. Verify that expected conflicts produce a stable domain outcome and no additional effect. Define how long keys remain replayable and what a client should do after that retention expires. Store only necessary outcome information under appropriate privacy controls. Reconciliation should read the authoritative operation or object state rather than inventing success from a client’s optimistic cache.
Common errors and fixes
A retry publishes twice
- Why it happens
- Transport retries had no stable business request identity.
- How to fix it
- Implement scoped replay recognition and reconcile the original result.
- Prevent it next time
- Test response loss after committed success.
Two duplicates both pass the initial lookup
- Why it happens
- Check and write were not atomically coordinated.
- How to fix it
- Use a durable uniqueness/transaction protocol for the selected store.
- Prevent it next time
- Review concurrent duplicate and crash boundaries.
A stale revision is confused with a replay
- Why it happens
- Object concurrency and request identity shared one field.
- How to fix it
- Define expectedRevision and requestKey separately.
- Prevent it next time
- Test a new edit and a retransmission independently.
Check your understanding
1. Why should the same request key with changed input fail?
Show worked solution
The key represents one logical request and its recorded outcome. Accepting changed input under that identity makes replay semantics ambiguous and can return an unrelated result or repeat a new effect invisibly. Bind the key to a canonical input fingerprint, principal and operation scope, and return a stable safe conflict when that binding does not match.
2. Does serial top-level mutation execution solve concurrent duplicate HTTP requests?
Show worked solution
No. The serial guarantee applies to top-level fields within an executed mutation operation, not a global lock across separate requests. Two requests may race in the domain service. Use the storage protocol’s atomicity and uniqueness mechanisms, then test duplicate races and uncertain response recovery in an isolated environment authorized for that integration.
Key takeaways
- Idempotency keys and revisions protect different write concerns.
- GraphQL mutation ordering does not implement durable replay safety.