Field notes / Startup & Product

Retries are a product feature: designing duplicate-safe workflows

How stable request identities, atomic persistence, and separate delivery states let a user retry without silently creating duplicate work.

A user presses Submit and the connection drops. They cannot tell whether the server saved the request, so they try again. A useful interface should help them recover without requiring them to understand which network step failed.

The backend needs the same clarity. “The request was saved” and “a notification was delivered” are separate facts. Combining them into one success flag makes recovery harder for both the user and the person operating the system.

Give a logical action a stable identity

For an unchanged retry, keep the same request key. On the server, bind that key to the form and the submitted content. If the action already exists with the same content, return its existing receipt rather than creating a second record.

If the user edits the content, treat it as a new logical action. Reusing a key for a different body should be rejected rather than silently overwriting the original request. The exact policy belongs in the API contract, not only in a button handler.

Visual field note / V11

One request identity, several attempts

Sequence showing two unchanged client attempts with one request key, a database insert-or-read operation, and the same saved receipt returned twice.

A stable key lets an unchanged retry find the same saved record. Changed content needs a new request identity.

Based on the documented workflow · Graphic by Nick Jensen
Read the diagram as text

Sequence showing two unchanged client attempts with one request key, a database insert-or-read operation, and the same saved receipt returned twice. A stable key lets an unchanged retry find the same saved record. Changed content needs a new request identity.

The sequence is a conceptual summary of the pattern used by this site’s inquiry flow. It is not a claim of exactly-once delivery across every downstream service.

Make persistence safe under concurrency

Checking whether a key exists and then inserting it as separate operations leaves a race. Two requests can both observe that the key is absent. The database needs to enforce the identity while the write happens.

The implementation here uses a deterministic inquiry ID and an atomic database write. A retry checks the stored request fingerprint before returning the existing receipt. The PostgreSQL INSERT documentation describes the conflict-handling primitive behind this class of implementation.

A useful concurrency test sends the same logical action several times and asserts that one record survives. A separate test changes the body while retaining the key and asserts a conflict. Testing only a second request after the first has completed misses the race.

Separate persistence from notification

Once the record exists, the operator can recover from a notification problem without losing the inquiry. Record the delivery outcome separately from the saved business record.

Visual field note / V12

Saved and notified are separate facts

State diagram in which a saved inquiry can have a disabled notification, pending notification, successful delivery, definite failure, or unknown delivery result.

The durable inquiry receipt survives a notification failure. An unknown delivery result needs investigation rather than a blind resend.

Based on the documented workflow · Graphic by Nick Jensen
Read the diagram as text

State diagram in which a saved inquiry can have a disabled notification, pending notification, successful delivery, definite failure, or unknown delivery result. The durable inquiry receipt survives a notification failure. An unknown delivery result needs investigation rather than a blind resend.

A confirmed success is different from a definite failure. A timeout can mean that the provider accepted the request but the acknowledgement never reached the caller. That outcome is unknown, not automatically safe to retry. Provider-supported idempotency or an investigation may be needed before a resend.

Disabled notifications should also be explicit. Otherwise a deliberately disconnected integration looks like a delivery defect, and someone may try to repair a system that is behaving as configured.

Give the user evidence they can keep

Return a receipt when persistence succeeds. Explain the next step in ordinary language. Do not display a success message merely because the browser finished sending a request, and do not claim an email was delivered when the server only saved a record.

Keep the receipt out of public analytics. A reference can help an operator locate an inquiry, but it does not belong in marketing events or a query string that will be copied between tools.

Test the recovery path, not just the happy path

Useful cases include a duplicate request arriving concurrently, an unchanged retry after a lost response, an edited body with a reused key, a definite provider failure, and an uncertain provider response. Keep test providers and databases isolated from live customer services.

This pattern is useful for inquiries, booking requests, and integration jobs, but the business rule still matters. A second booking with different details may be intentional. Stable identity should prevent an accidental duplicate without preventing a legitimate new action.

The API-First Architecture guide offers a broader view of contracts between systems. If an integration needs a clearer failure and recovery story, discuss a build.

From the idea to the work

Make the workflow useful in a real product.

Bring a product idea, an internal tool, or an AI-assisted workflow that needs a working implementation.

See the Lively Icons case study →


More Stories