Identify the intended operation
Use a stable request identifier for retries of the same intended submission. Generate a new identifier when the person changes the submission into a different operation. Disabling a button while a request runs helps the interface, but does not cover every source of duplicate requests.
The server needs to associate the identifier with the original operation and detect conflicting reuse. AWS’s discussion of idempotent APIs explains why caller intent and repeated requests need an explicit contract. The application still has to define what “the same submission” means for its own workflow. AWS Builders’ Library: idempotent APIs.
Distinguish recording from side effects
Saving an inquiry and sending an email are separate effects. Track their outcomes so a retry can resume unfinished work rather than blindly repeating everything. A local success record alone cannot prove that an external service received nothing during a timeout.
Choose an honest policy for ambiguous outcomes. Some operations can tolerate a possible duplicate carrying the same reference; others require checking the external system before trying again. Do not promise exactly-once behavior merely because the browser reuses a token.
Explain uncertainty in the interface
Keep the person’s input and provide a clear retry or status-check path when confirmation is unavailable. Reuse the operation identifier so that returning to the request can confirm its existing result.
Test interrupted connections, repeated clicks, and a failure between local storage and the external action. Inspect the resulting records, not just the success message. A reliable submission flow is defined by what happens during uncertainty as much as by what happens on the first successful click.
Describe the operation as a state machine.
A retry design needs to explain more than repeated clicks. Two requests can overlap, a process can stop after saving a record, and a caller can reuse a key with different content. The AWS Builders’ Library discussion of idempotent APIs explains why a caller-provided identifier can express intent more clearly than assuming identical payloads always represent the same operation.
- ClaimReserve the operation key for this caller.
- CommitSave the business result and completion record.
- ReplayReturn the saved result for a matching retry.
In a database-backed inquiry service, scope the key to the authenticated account or other appropriate caller context. Record a normalized payload fingerprint so the service can distinguish a retry from an incompatible reuse. That fingerprint supports comparison; it is not a substitute for the operation key. Two deliberately separate inquiries may contain identical text.
| Stored state | Incoming request | Defined outcome |
|---|---|---|
| No record | New key | Attempt an exclusive claim. |
| In progress | Matching key and payload | Return a documented pending response or wait within a bound. |
| Complete | Matching key and payload | Return the original result. |
| Any existing state | Same key, different payload | Reject the conflict rather than silently accepting changed intent. |
Find the gap between saving and notifying.
# Illustrative transaction boundary, not production code
begin transaction
claim unique (caller_id, operation_key)
save inquiry
save notification work item
save completed result for operation_key
commit
worker reads notification work items
worker records each provider responseThe unique claim and business changes need a coherent transactional design. A uniqueness constraint alone does not decide how an interrupted in-progress operation is recovered. Define ownership, timeout, and reconciliation rules for that state. Also keep the distinction between a committed inquiry and a notification accepted by an email provider.
An outbox can make intended notification work durable, but it does not automatically guarantee exactly-once delivery by an external service. A worker can lose the response after the provider accepts a message. Test that uncertainty explicitly, and document the remaining duplicate risk. Include simultaneous retries, a worker crash, conflicting payloads, and expiration of the deduplication record in the verification plan. The retention period should be a business decision, not an unexplained cleanup constant.
A practical example.
A user submits a service request and loses their connection before seeing confirmation. The application may already have created the request. If the next click creates another record, the team receives duplicates even though the user followed reasonable recovery steps.
Treat retries of the same submission as the same intended operation, using a design appropriate to the backend. Define what happens when a retry carries changed content and how long the original result remains recognizable. The user interface still needs to communicate uncertainty accurately: disabling a button helps avoid accidental clicks but does not resolve a network failure. Test both a repeat arriving during processing and one arriving after the original request has completed.
Put it into practice.
- Identify the business operation whose repeated execution would create harm.
- Define repeated-request behavior and how changed payloads are handled.
- Test the gap between a committed result and a lost confirmation response.