Idempotency is a user experience feature
Z29C made repetition safe across lost responses and worker retries, turning “try again” from hopeful interface copy into a protocol backed by durable receipts.
The button said Something went wrong. Try again. The synthetic delivery had already succeeded.
The browser had submitted a document workflow to Z29C. The API committed the request and returned a workflow identifier. The response disappeared during a network interruption. With no receipt, the page displayed an error. Pressing the offered button created another workflow and delivered the same document again.
Nothing about the interaction looked reckless. The copy was calm, familiar, and wrong.
If a product asks a person to repeat an action, the product should carry the identity and evidence needed to make repetition safe. That requirement reaches from a button through API storage, worker execution, external adapters, and recovery.
Idempotency is usually introduced as a backend property. In Z29C it became a user-experience feature: the ability to try again without fear.
The button could not prevent the duplicate
My first interface disabled Submit after the click. That stopped a fast double-click in one tab. It did not survive:
- The response being lost before the page learned the workflow ID.
- A page refresh that reset component state.
- The same action opened on another device.
- A browser back navigation that restored an old enabled control.
- A worker retry duplicating a later external step.
- A person assuming the first error meant no request existed.
Debouncing and disabled states remained useful feedback. They were not identity.
The browser needed to generate a stable client operation key when the person confirmed the action. Every transport retry for that action reused it. A new deliberate action received a new key.
The server stored the key with the immutable normalized command and workflow in one transaction. The safety boundary no longer depended on one page remembering that it had clicked.
Interface state made repetition less likely. Durable identity made it safe.
Identical input was not identical intent
I initially hashed the request payload and used the digest as the idempotency key. That seemed elegant and conflated two concepts.
A person might deliberately generate the same document twice for separate test scenarios. Those actions had equal input and distinct intent. Conversely, one retry might include a changed client timestamp or harmless metadata and still represent the same original action.
The client key identified intent. The normalized payload digest verified consistency under that key.
Z29C handled repeats:
type StartResult =
| { status: 'accepted'; workflowId: string; receiptRevision: number }
| { status: 'already-accepted'; workflowId: string; receiptRevision: number }
| { status: 'key-conflict'; expectedDigest: string }The same key and equivalent command returned the existing workflow. The same key with different semantic input was rejected. Z29C did not silently update the earlier workflow or create another.
The key was scoped to initiating subject and operation type. A collision across different people or commands could not expose another workflow.
Identity came from the user's action; content equality remained supporting evidence.
The browser's operation key prevented duplicate workflow creation. It did not automatically make every workflow step idempotent.
Z29C had several identity layers:
- Client operation key for recording one user intent.
- Stable workflow ID for its lifetime.
- Stable step ID from the versioned definition.
- Attempt ID for each worker execution.
- External idempotency or client reference for one side effect.
The delivery step derived its external key from workflow and stable step identity, not from attempt. Worker attempts could repeat while the intended external effect remained one.
A document-generation step could use workflow, step, and input digest as its artifact identity. If generation was deterministic, repeated attempts found or replaced the same immutable result safely.
I avoided passing the browser's original key everywhere as a universal identifier. One workflow could legitimately perform several distinct side effects. Keys needed a scope whose meaning the receiver could enforce.
End-to-end safety came from related identities, not one magical token.
The API committed the receipt before success copy appeared
The browser could only trust Request recorded after Z29C had durably committed the mapping from client key to workflow.
The API transaction created:
- Idempotency-key record and command digest.
- Workflow intent and definition version.
- Initial state.
- Transactional outbox event for scheduling.
If the transaction failed, no workflow existed under the key. If it committed and the response vanished, repeating returned the receipt.
The response included a stable workflow URL. The browser stored the pending key locally before sending, so a refresh could query or retry. After it learned the workflow ID, the local pending record collapsed into the durable server identity.
I did not acknowledge on queue publication or worker start. Those could happen later. The product promise at this point was smaller: Z29C has recorded the request exactly once under this identity and will expose its progress.
Clear copy followed a clear commit boundary.
Z29C could not retain every client key forever. Storage and privacy policies required an end.
The key's deduplication window needed to cover realistic browser retry, offline outbox, and workflow retention. The API documented it in the receipt. A client holding a pending operation longer than that window could not blindly submit it as if deduplication still existed.
Before expiry, the client could query outcome. Near expiry, it surfaced attention. After expiry, the person chose whether to create a new deliberate action, with prior evidence displayed.
The workflow record could outlive the API key or be archived under a different retention policy. A compact key tombstone could preserve conflict detection for a bounded period without retaining the full request payload.
Deleting an account or synthetic subject followed explicit lifecycle jobs. Idempotency data did not become an undocumented permanent ledger.
“Safe to retry” is always safe under a scope and time window. Hiding that window moves risk into the oldest and weakest clients.
A destination had to participate
Z29C could send an Idempotency-Key header to an external adapter. The header did nothing unless the destination stored and enforced it.
The strongest synthetic adapter committed key, normalized request digest, external reference, and result together. Repeating the same key returned the same reference. A changed request under one key was rejected. Concurrent duplicate calls also converged on one result.
Contract tests forced response loss after acceptance, sent the same key from two attempts, and varied the payload. The adapter passed only if one external effect existed.
For destinations without idempotent creation, Z29C used a stable client reference and query where available. If a status lookup found the prior effect, Z29C attached its receipt. If the destination exposed neither deduplication nor lookup, outcome unknown required review.
The product could not promise safe automatic retry beyond the actual adapter contract.
Idempotency is cooperative. The caller supplies stable identity; the receiver makes it durable.
Most tests made an adapter return success or throw before doing anything. That made retry look safe by construction.
The important scenario was:
- Destination accepts the request and commits external reference
x-82. - Response is delayed or lost.
- Worker records no completion receipt and its lease expires.
- Recovery repeats with the same key or queries by reference.
- Destination returns
x-82rather than creatingx-83. - Z29C commits one step receipt and advances once.
I ran the failure after every checkpoint: request identity commit, network write, destination acceptance, response read, local receipt commit, and queue acknowledgement.
If the local receipt existed and the queue redelivered, the handler returned without invoking externally. If only request identity existed, adapter policy governed recovery.
The scenario tested the moment where interface uncertainty and system reality diverged. That was the point idempotency existed to repair.
Unknown remained necessary
Idempotency reduced uncertainty where the destination honored the key. It did not make every timeout success or failure.
An adapter could reject the key, lose its own deduplication store, apply only a short window, or return an ambiguous status. A network failure could occur before Z29C knew which capability version handled the request.
Z29C kept outcome-unknown as a first-class step state. It stored available evidence and blocked dependants requiring the receipt. The recovery console offered only actions justified by the adapter:
- Query the stable reference.
- Retry the same key within its valid window.
- Attach verified manual evidence.
- Authorize a duplicate-risk attempt explicitly.
- Stop and compensate other completed steps where useful.
Idempotency was not a reason to collapse unknown. It was one mechanism that made many unknown states resolvable without fear.
Truthful uncertainty protected the user experience from confident duplication.
The API's idempotency logic initially performed a read followed by insert. Two concurrent submissions could both see no key and create workflows.
A unique constraint on the scoped client key established one durable owner. The transaction attempted insertion and handled conflict by loading the existing record and comparing command digest.
The external synthetic adapter used an equivalent unique key boundary in its own store. Its result association committed atomically with acceptance.
Application checks remained useful for clear errors. The storage constraint defended concurrency.
I tested simultaneous submissions rather than only sequential retry. Both responses returned the same workflow ID. One workflow event entered the outbox. A key conflict with different data returned an error without exposing the stored command.
The user-facing promise rested on more than a convention in controller code. It reached the database race where two attempts actually met.
The receipt turned a timeout into navigation
The original page displayed an error because it had not received success. With a stored local operation key, response loss produced a recovery state:
Checking whether your request was recorded…
The client queried by key. If Z29C returned the workflow, the page navigated to it. If the server had no record and the key remained valid, it resubmitted the same command. If connectivity remained unavailable, the outbox preserved the operation.
The person did not need to decide whether clicking again was dangerous. The product performed the safe sequence.
The workflow page showed meaningful milestones rather than a frozen submission spinner. Another device could open the same workflow URL. Repeated navigation did not repeat the command.
The receipt reduced more than duplicates. It reduced ambiguity after an error, which reduced anxious manual checking and accidental new actions.
Reliable storage changed the shape of the interface.
I audited every Retry label in Z29C.
Some meant re-fetch a read-only view. Some meant reattempt a pure calculation. Some meant repeat an external request under a stable idempotency key. Some were actually query, resume, reauthenticate, or create a new corrected workflow.
The labels became specific:
- Check status when the external effect might exist.
- Retry delivery safely only when the same key remained enforceable.
- Resume preparation from a local checkpoint.
- Sign in to continue when delivery was blocked on authentication.
- Create corrected request for materially changed input.
- Request withdrawal for an already completed effect.
Where a simple Retry remained, the system could explain what identity it reused and what outcome was guaranteed.
Copy became an honest summary of protocol. It did not create safety through confidence.
Safe retry changed cancellation too
One stable workflow identity made cancellation less ambiguous. The browser no longer tried to cancel whichever job ID it had most recently received.
A cancellation request had its own idempotency key and targeted the workflow. Repeating it returned the same transition or current state. Z29C checked what effects had already completed.
Before any external effect, cancellation could prevent the requested outcome. After delivery, the product offered withdrawal or stop-remaining-work with explicit limits. During unknown outcome, it stopped future steps and preserved investigation rather than claiming success.
Compensation requests also needed stable identity. A timed-out withdrawal should not produce several withdrawals or contradictory restoration attempts.
Idempotency therefore supported corrective actions, not only initial submission.
The broader rule was that every consequential button represented intent and deserved identity across response loss.
Even a technically idempotent operation can consume resources. The destination may parse, authenticate, look up the key, and return a result on every retry. Repeating too aggressively can trigger rate limits or amplify an outage.
Z29C used bounded backoff with jitter, honored adapter Retry-After guidance, and enforced attempt and workflow budgets. A valid idempotency key made repeated effect safe; it did not make repeated traffic useful.
The key window could expire. A destination might guarantee deduplication for 24 hours while Z29C's workflow waited longer. Recovery checked current validity before retry.
Some repeated operations returned the original result but not the original side-channel behavior. A destination might suppress duplicate delivery while emitting another audit event or consuming quota. Adapter contracts documented such limits.
Safety and scheduling remained separate decisions.
The user experience benefited from quiet recovery, not an invisible storm of harmless duplicate requests.
The delivery workflow assembled and stored a document before sending it. Retrying generation could produce different bytes if templates, timestamps, or dependencies changed across deployment.
Z29C stored the normalized input, definition version, template identity, and generation policy. The artifact received a content digest and immutable location. The delivery step referenced that digest.
If generation retried under the same logical step, deterministic inputs had to yield equivalent output or the step stopped with an integrity error. A corrected template created a new step or workflow revision; it did not replace bytes under an existing delivery key.
This prevented an external idempotency key from referring to two document versions. The destination could not safely deduplicate requests whose semantic payload changed.
Idempotency began before the external request. Every derived artifact participating in identity needed stable lineage.
The feature connected build-manifest lessons from R7K1 with workflow receipts.
Manual resolution used a new receipt, not a flag
When an opaque adapter left outcome unknown, a reviewer could verify the destination through available evidence. The first console offered Mark successful.
That control could turn any step green without saying what had happened.
Z29C required a typed manual receipt: external reference if available, evidence link or bounded observation, author, time, input digest, and reason the evidence satisfied the step. The action had its own idempotency key so a lost response did not create repeated transitions.
If evidence remained incomplete, the reviewer could waive the uncertainty or authorize a duplicate-risk retry, each with explicit residual risk. The workflow projection showed manual resolution.
The receipt did not pretend automation had verified the result. It supplied durable evidence sufficient for the chosen conclusion.
Idempotency reduced the need for manual decisions. Where it could not, the same identity discipline made those decisions accountable.
Attempt-level success could hide duplicates. Z29C measured:
- Duplicate client submissions returning one workflow.
- Key conflicts indicating client misuse.
- Attempts per logical step.
- One-effect steps with more than one external reference.
- Unknown outcomes resolved by idempotent retry, query, or review.
- Time from user intent to durable receipt.
- Expired key windows encountered during recovery.
No fabricated production volume appeared. The synthetic harness produced known scenarios and asserted invariants.
A workflow with two deliveries and a final successful attempt was not successful under the one-effect invariant. A workflow with a timeout, safe idempotent replay, and one external reference was.
The metric denominator aligned with the person's requested outcome rather than the queue's job attempts.
Idempotency changed what “success” meant operationally.
Recovery needed to remain accessible after interruption
The technical protocol could be correct while the person using a keyboard or screen reader encountered two workflows in the interface. I tested response loss as an interaction, not only an API sequence.
After Submit, focus moved to a status heading associated with the locally pending operation. The live region announced Checking whether your request was recorded once; repeated polling did not produce repeated speech. When the server returned the existing receipt, the route changed to the durable workflow and focus stayed on its current milestone rather than jumping to the page top without explanation.
If outcome remained unknown, the page exposed one named review region with the original intent, latest evidence, and permitted actions. A disabled Retry control included a visible reason. When safe retry became available after status lookup, the update notice did not steal focus from someone reading attempt details.
The browser history also preserved identity. Back navigation to the original form showed that the operation had already created workflow w-17 and linked to it. It did not silently restore an enabled blank form that looked like the request had never happened. Starting another deliberate action required a clearly labeled new submission.
On a second device, the workflow page used the same durable milestones and receipts. No client-side animation was required to keep the run alive or to reconstruct completion.
These details made the backend property perceivable. Idempotency that exists only in storage but presents duplicate cards, repeated announcements, or an ambiguous back button has not finished becoming a user-experience feature.
Z29C could not promise exactly-once effects across destinations that did not participate. A provider might violate its documented key behavior. A manual action outside Z29C could create a duplicate. A key retained too briefly could expire before an offline client returned.
The product kept those boundaries visible. Adapter contract versions appeared in receipts. Unknown outcome remained available. Long-lived pending operations warned before key expiry. Duplicate detection could link external references discovered later.
Idempotency also did not decide whether the user's original request was wise, authorized, or still useful. Current policy and workflow deadlines still applied.
The feature was narrower than perfection and more valuable than a vague “exactly once” promise.
It made a specific interaction safe under named conditions.
Reliability created an emotional affordance
Before Z29C, an error after submission created hesitation. Had the action happened? Would another click make it worse? Should the person search for the document, wait, refresh, or start again?
After stable identity and receipts, the system could answer. It could recover automatically, show one workflow, and distinguish waiting from unknown. Retry became available only where its semantics were known.
That changed how the interface felt. A safe retry invited action without requiring courage. A durable receipt reduced the urge to inspect another system for secret success. A truthful unknown state allowed careful review without manufacturing failure.
These are emotional outcomes produced by unique constraints, immutable commands, external keys, and step receipts.
The original button had treated “try again” as reassurance. Z29C made it a capability. If software asks a person to repeat themselves, software should be the part that remembers it has heard them before.
That memory needs a scope, a lifetime, and a receipt. Without all three, confidence returns to interface copy precisely where the system should be carrying evidence.
The safest button is backed by a system that can recognize the same intention.