All writing

Idempotency: designing systems that can safely try again

Distributed operations are often delivered or attempted more than once. Idempotency gives repeated attempts one stable identity and prevents them from multiplying the intended business effect.

about 31 minutes min read

Suppose checkout asks the order service to create an order.

CHECKOUT
    │
    │ CreateOrder
    ▼
ORDER

Order creates:

order-5001

and commits it successfully.

Before the response reaches checkout, the connection fails.

CHECKOUT                       ORDER

CreateOrder
    ─────────────────────────►

                               create order-5001
                               commit transaction

    connection fails

no response received

Checkout now knows:

I do not have a successful response.

It does not know:

The order was not created.

The first attempt may have failed before reaching order.

It may have succeeded completely while only the response was lost.

Checkout retries.

Without any duplicate protection, order may create:

order-5002

The customer clicked once.

The network attempted twice.

The system created two orders.

That is the problem idempotency is meant to solve.

Idempotency allows several attempts at one logical operation to produce no more than the intended business effect.

The retries can remain separate network interactions.

The business operation remains one operation.

Repetition happens even when nobody asks for it

Retries are an obvious source of repeated work.

They are not the only source.

The same operation may be attempted again because of:

client retry

load-balancer retry

service retry

message redelivery

consumer restart

worker crash

operator replay

dead-letter recovery

duplicate user submission

scheduled reconciliation

A message broker may deliver a record again when a consumer completes its database work but fails before acknowledging the message.

A browser may submit a form twice after the first response appears slow.

An operator may replay a failed event without knowing that the original handler completed part of its work.

Distributed applications should therefore assume:

the same logical work may arrive more than once

That assumption is safer than attempting to prove that every request and message will always be delivered exactly once across every participating system.

Idempotency is about the effect

In mathematics, an idempotent operation has the property:

f(f(x)) = f(x)

Applying it again does not change the result after the first application.

A familiar example is setting an absolute value:

set product status to DISCONTINUED

Applying that operation twice still leaves the product discontinued.

DRAFT
  │
  │ SetStatus(DISCONTINUED)
  ▼
DISCONTINUED
  │
  │ SetStatus(DISCONTINUED)
  ▼
DISCONTINUED

By contrast:

increase stock by 5

is not naturally idempotent.

quantity 10
    │
    │ AddStock(5)
    ▼
quantity 15
    │
    │ AddStock(5)
    ▼
quantity 20

The second application creates another effect.

In distributed applications, the useful definition is broader than repeating one pure function.

The system may perform several internal steps while still preserving one externally meaningful result.

For example:

attempt 1:
    create order-5001
    response lost

attempt 2:
    recognise completed operation
    return order-5001

The second attempt does not rerun every internal instruction.

It preserves the intended business outcome:

one checkout operation
    │
    ▼
one order

Idempotent does not mean every response is identical

Repeated attempts may produce different transport-level responses.

The first attempt might return nothing because its connection failed.

The second might return:

order_id:
    order-5001

status:
    pending

A later attempt might return the same order with a newer representation:

order_id:
    order-5001

status:
    confirmed

Whether that is acceptable depends on the contract.

One API may promise to return the original operation result.

Another may return the current resource state.

The essential property is that retrying the same logical operation does not create another unintended business effect.

same operation

does not require

byte-for-byte identical response

The response policy should still be documented so callers know what repeated attempts return.

Some operations are naturally idempotent

An operation can be idempotent because of its meaning.

Reading current state

GetProduct chair-42

Repeating the query does not intentionally create another product.

The returned state may change between attempts because the product itself changes.

The operation remains a read.

Setting an absolute state

SetProductStatus(DISCONTINUED)

Repeated applications target the same final state.

Deleting a known resource

DeleteBasket basket-71

Once the basket is deleted, another deletion may return:

already absent

or:

success

depending on the API contract.

The important effect is that the basket remains absent.

Replacing a complete representation

PutShippingAddress(address-12)

Repeating the same replacement can produce the same stored representation.

These operations still need concurrency and validation rules.

Natural idempotency only means repetition does not inherently accumulate another copy of the effect.

Some operations require an idempotency identity

Operations such as these are not naturally safe to repeat:

CreateOrder

ReserveStock

CapturePayment

IssueRefund

SendNotification

AddLoyaltyPoints

The service needs to distinguish:

another attempt at the same operation

from:

a genuinely new operation

The payload alone may not provide enough information.

Two customers can legitimately order the same items.

One customer can legitimately place the same order twice on different days.

Two refunds may have the same amount but relate to different decisions.

The caller therefore supplies a stable operation identity.

idempotency_key:
    checkout-7f82

or:

operation_id:
    payment-capture-91

Every attempt belonging to the same logical operation uses the same identifier.

LOGICAL OPERATION

checkout-7f82
│
├── request attempt 1
├── request attempt 2
└── request attempt 3

A genuinely new customer action receives a new identifier.

checkout-7f82:
    first order attempt


checkout-a419:
    later separate order

The identifier defines the deduplication boundary.

The caller should create the operation identity

The caller knows whether two attempts belong to the same intention.

Suppose a storefront begins one checkout.

It can create:

checkout_id:
    checkout-7f82

before contacting order.

USER SUBMITS CHECKOUT
          │
          ▼
STOREFRONT CREATES OPERATION ID
          │
          ▼
CreateOrder(checkout-7f82)

If the request times out, the storefront retries with:

checkout-7f82

If the customer later starts another checkout, the storefront creates a new ID.

The order service cannot reliably infer this relationship from timing or payload equality.

Two requests arriving one second apart might be:

one retry

or:

two legitimate purchases

Caller-provided identity makes the intention explicit.

The key needs a defined scope

An idempotency key is not meaningful without a namespace.

Suppose two customers both use:

operation_id:
    123

Those operations should not collide.

A deduplication identity may include:

caller identity
+
operation type
+
idempotency key

For example:

customer:
    customer-72

operation:
    CreateOrder

key:
    checkout-7f82

The stored identity becomes conceptually:

customer-72 / CreateOrder / checkout-7f82

The correct scope depends on the contract.

Possible scopes include:

per customer

per tenant

per API credential

per service

per operation type

globally unique

The API should define the scope rather than leaving clients to discover collisions experimentally.

The same key should represent the same request

Suppose the first attempt uses:

operation_id:
    checkout-7f82

basket_id:
    basket-11

total:
    £249.00

A later attempt reuses the same key with:

operation_id:
    checkout-7f82

basket_id:
    basket-99

total:
    £799.00

The service should not quietly treat the second payload as another version of the first operation.

That would make the idempotency identity ambiguous.

A robust service can store a request fingerprint or the significant request fields.

operation:
    checkout-7f82

request fingerprint:
    8b91...

On another attempt:

same key
same request meaning
    │
    ▼
return existing result

But:

same key
different request meaning
    │
    ▼
reject conflict

The response might report:

idempotency key already used
with different request data

This protects both the service and the caller from accidentally attaching two intentions to one identity.

The service needs a durable idempotency record

The service can store something resembling:

IDEMPOTENCY RECORD

operation_id:
    checkout-7f82

request_hash:
    8b91...

status:
    completed

resource_id:
    order-5001

response:
    accepted order result

When the request first arrives:

lookup checkout-7f82
        │
        ├── absent
        │      └── begin operation
        │
        └── present
               └── return recorded outcome

A simple table might contain:

CREATE TABLE idempotency_operations (
    operation_id VARCHAR(128) PRIMARY KEY,
    request_hash VARCHAR(128) NOT NULL,
    operation_status VARCHAR(32) NOT NULL,
    resource_id VARCHAR(128),
    response_payload BLOB,
    created_at TIMESTAMP NOT NULL,
    completed_at TIMESTAMP NULL
);

The exact schema depends on the service.

The important properties are:

unique operation identity

recorded request meaning

processing state

authoritative outcome

retention information

Recording the key after the effect is too late

Consider this sequence:

1. create order

2. record idempotency key

The service crashes between the steps.

ORDER DATABASE

order-5001 exists


IDEMPOTENCY TABLE

checkout-7f82 absent

The retry arrives.

The service sees no idempotency record and creates order-5002.

The duplicate-protection mechanism failed in the exact failure window it was meant to cover.

Where possible, the business effect and idempotency record should be committed atomically.

BEGIN TRANSACTION
  │
  ├── create order-5001
  └── record checkout-7f82 → order-5001
COMMIT

Now either both are committed or neither is.

COMMIT SUCCEEDS

order exists
idempotency result exists


COMMIT FAILS

neither exists

A retry can determine the outcome from the same authoritative database.

Reserving the key before work introduces an in-progress state

Another design first creates the idempotency record:

checkout-7f82:
    processing

Then it performs the operation.

This prevents a second concurrent request from beginning the same work.

attempt A:
    creates processing record


attempt B:
    sees processing record
    does not begin duplicate work

The service must still handle a crash after reserving the key.

IDEMPOTENCY RECORD

checkout-7f82:
    processing


BUSINESS EFFECT

unknown

The operation may have:

not started

partially completed

completed in another system

completed locally but not recorded

The service therefore needs a policy for stale in-progress records.

Possible responses include:

wait briefly for current processing

return an operation-pending result

query authoritative state

resume the operation

mark it failed after reconciliation

require operator repair

An in_progress flag is not a complete recovery strategy.

It is an honest representation that another attempt may currently own the operation.

Concurrent duplicates need a database-enforced winner

Two identical attempts may arrive at nearly the same moment.

attempt A ──► service

attempt B ──► service

Both check for the key.

Both see it as absent.

Without atomic protection, both continue.

A:
    no key found

B:
    no key found

A:
    create order-5001

B:
    create order-5002

Application-level “check then insert” logic is vulnerable to this race.

A unique constraint or atomic conditional write should decide which attempt owns the key.

INSERT operation checkout-7f82
        │
        ├── A succeeds
        └── B receives duplicate-key result

Attempt B then reads the existing operation state instead of starting another effect.

The database becomes the authority for:

which attempt acquired the logical operation

This is stronger than relying on two processes to inspect and update separate in-memory maps politely.

A key can map to a pending operation

Not every operation completes immediately.

Suppose a payment provider takes time to return a final outcome.

The idempotency record may move through:

RECEIVED
    │
    ▼
PROCESSING
    │
    ├──► SUCCEEDED
    ├──► FAILED
    └──► OUTCOME_UNKNOWN

Repeated requests can return the current operation status.

operation_id:
    payment-91

status:
    processing

The caller may later query:

GetPaymentOperation(payment-91)

This separates:

attempting to start the operation

from:

observing its eventual result

For long-running or externally dependent work, an operation resource can be more truthful than keeping one request open or repeatedly restarting the same work.

Idempotency and reconciliation belong together

An idempotency key helps the service recognise the operation.

It may not reveal the outcome automatically.

Suppose payment sends:

CapturePayment

operation_id:
    payment-capture-91

The provider captures the money.

The application loses the response before recording the provider result.

Its local record says:

payment-capture-91:
    processing

The provider says:

captured

Retrying with the same provider idempotency key may return the original result.

If it does not, the application may need to query the provider.

LOCAL STATE

unknown
     │
     ▼
QUERY PROVIDER
     │
     ▼
AUTHORITATIVE CAPTURE RESULT
     │
     ▼
UPDATE LOCAL OPERATION

Idempotency prevents a second capture.

Reconciliation restores knowledge about the first one.

These are related but different responsibilities.

Retention defines how long retries remain safe

The service cannot necessarily retain every idempotency record forever.

It needs a retention policy.

Suppose records are retained for:

24 hours

A retry within that period can recover the earlier result.

A retry after expiry may look like a completely new operation.

day 1:
    checkout-7f82 creates order-5001


day 3:
    checkout-7f82 record no longer retained
    request arrives again

The service needs a documented rule.

Options include:

reject keys older than a known client timestamp

retain records for the maximum retry and replay window

use a business identifier that remains unique permanently

query the created resource through another stable identity

The retention period should consider:

client retry duration

message retention

dead-letter recovery window

operator replay procedures

business risk of duplicate effects

storage cost

privacy obligations

An idempotency promise lasts only as long as the evidence used to enforce it.

Natural business keys can provide permanent protection

Sometimes the business operation already has a stable unique identity.

For example:

one order per checkout_id

The order table can enforce:

UNIQUE (checkout_id)

Then:

checkout-7f82

cannot create two orders even if the separate idempotency record expires.

Similarly:

one refund per approved refund_id

or:

one notification of type ORDER_CONFIRMED per order_id

can be protected by unique constraints.

This is often stronger than a generic short-lived request cache because the uniqueness lives with the business record.

BUSINESS INVARIANT

one order for one checkout


DATABASE CONSTRAINT

checkout_id is unique

A generic idempotency table remains useful for storing response and attempt state.

The domain-level uniqueness protects the effect itself.

Idempotency should be attached to the logical command

Suppose a checkout workflow calls:

CreateOrder

ReserveStock

AuthorisePayment

Each operation may have its own identity.

checkout operation:
    checkout-7f82

order creation:
    order-create-31

stock reservation:
    reservation-71

payment attempt:
    payment-91

Reusing one universal identifier everywhere may be convenient.

It can also blur different retry scopes.

For example, the checkout may retain one correlation ID:

correlation_id:
    checkout-7f82

while each command has its own operation ID.

ReserveStock

operation_id:
    reservation-71

correlation_id:
    checkout-7f82

The correlation ID says:

These messages belong to one wider workflow.

The operation ID says:

These attempts belong to one logical command.

The distinction helps when one workflow legitimately performs several stock reservations or payment attempts.

Message consumers need idempotency too

Suppose Kafka delivers:

OrderConfirmed

event_id:
    event-44

The notification consumer creates an email job.

It then crashes before recording its consumer progress.

Kafka delivers event-44 again.

delivery 1:
    notification job created

delivery 2:
    notification job created again

The event describes one order confirmation.

The consumer has created two reactions.

A consumer can protect itself using an inbox or processed-message record.

BEGIN TRANSACTION
  │
  ├── insert processed event-44
  └── create notification job
COMMIT

A unique constraint on event_id ensures only one transaction acquires the event.

On redelivery:

event-44 already processed
        │
        ▼
do not create another job

The processed marker and local effect should be committed together.

Otherwise the consumer can record the event as processed while failing to create the effect, or create the effect while failing to record the event.

A processed-event table is not always enough

Suppose the consumer records:

event-44:
    processed

Then an operator discovers that the handler contained a bug.

The event must be replayed after the fix.

The processed-event marker now causes the corrected handler to skip it.

The recovery process needs a deliberate policy.

Possibilities include:

remove the processed marker under controlled conditions

use handler-version-aware processing records

rebuild the complete projection from scratch

apply a targeted repair instead of ordinary replay

publish a corrective event

Deduplication protects normal delivery.

Recovery may intentionally need to process the information again.

The design should distinguish:

accidental duplicate

from

authorised replay or repair

Business-effect uniqueness can be better than message deduplication

Suppose notification handles two different events:

event-44:
    OrderConfirmed


event-51:
    OrderConfirmationRequested

Both could lead to the same email.

Deduplicating only by event ID permits two jobs because the events have different IDs.

The protected business effect may be:

one order-confirmation email for order-5001

A unique constraint such as:

order_id + notification_type

protects the actual outcome.

event-44 ──┐
           ├──► ORDER_CONFIRMATION / order-5001
event-51 ──┘

This suggests two complementary identities:

MESSAGE IDENTITY

Have I processed this delivery before?


BUSINESS-EFFECT IDENTITY

Have I already produced the effect
these messages are requesting?

The second can protect the system even when several message paths converge on the same result.

Event consumers should prefer target-state updates

Suppose a projection consumes:

ProductPriceChanged

product_id:
    chair-42

current_price:
    £249.00

product_version:
    19

The consumer can write:

set chair-42 price to £249.00 at version 19

Repeating that update produces the same state.

A weaker event might say:

increase chair-42 price by £20.00

Reapplying it would increase the price twice.

Absolute target state is often easier to process idempotently than a relative mutation.

Similarly:

set order status to CONFIRMED at version 7

is easier to protect than:

advance order status

The event should still describe a meaningful fact.

Where appropriate, carrying the accepted target state and entity version gives consumers stronger replay and deduplication options.

Entity versions protect against stale repetition

Suppose a consumer has already applied:

product version 20

It receives a delayed duplicate or older event:

product version 19

The consumer can compare:

incoming version:
    19

current version:
    20

and refuse to overwrite newer state.

19 < 20
    │
    ▼
ignore stale update

This provides a form of idempotent and monotonic processing.

The same version arriving again has no further effect.

An older version cannot move the projection backwards.

The version must be assigned by the authoritative owner and have a clear scope.

A timestamp generated by several services is usually a weaker ordering identity.

Database transaction retries need idempotent boundaries

A database may abort a transaction because of:

deadlock

serialization failure

optimistic concurrency conflict

The application may retry the complete transaction.

BEGIN
  │
  ├── read current state
  ├── make decision
  ├── write state
  └── commit

If commit fails with an eligible concurrency error, the application repeats:

read

decide

write

commit

This can be safe when all effects remain inside the transaction.

It becomes dangerous when the transaction also performs:

send email

publish directly to broker

call payment provider

write external file

Those external effects are not rolled back with the database transaction.

attempt 1:
    send email
    database transaction aborts

attempt 2:
    send email again
    database transaction commits

A retriable database transaction should normally avoid uncontrolled external side effects.

Instead, it can record durable local work such as an outbox entry.

BEGIN
  │
  ├── update order
  └── insert OrderConfirmed outbox record
COMMIT

The outbox publisher handles delivery separately and idempotently.

HTTP method semantics help, but they do not finish the design

HTTP classifies methods such as:

GET

PUT

DELETE

as idempotent in their intended semantics.

Repeated identical PUT requests set a resource representation.

Repeated DELETE requests leave the resource deleted.

POST does not carry the same general idempotency guarantee.

But method choice alone cannot protect a business effect.

A PUT handler can still:

send a new email on every call

append another audit row incorrectly

charge a card each time

publish duplicate events without identity

A POST operation can be made safely repeatable using an idempotency key.

Protocol semantics communicate expectations.

The application must implement them across its actual effects.

Idempotency does not mean ignoring all duplicates

Suppose a second attempt arrives while the first is still running.

Possible responses include:

wait for the first attempt

return OPERATION_IN_PROGRESS

return the known operation resource

reject concurrent duplicate temporarily

Silently dropping the second request may leave the caller without any result.

The caller needs enough information to decide whether to wait, poll or stop.

Likewise, a message consumer should not always acknowledge a duplicate without checking that the original local effect completed.

processed marker exists

but

business effect missing

would reveal a broken atomicity assumption.

Idempotency is not:

duplicate seen
    │
    ▼
throw it away

It is:

duplicate seen
    │
    ▼
identify authoritative outcome
    │
    ▼
return or preserve that outcome safely

Idempotency does not correct invalid work

Suppose a caller sends:

quantity:
    0

with an idempotency key.

The service should return:

INVALID_ARGUMENT

Repeating the same invalid operation can return the same rejection.

That is consistent.

The key does not make the request valid.

Similarly, idempotency does not solve:

permission denied

missing product

failed business precondition

corrupt message

unsupported schema

It prevents repeated attempts from multiplying effects.

Other validation and recovery mechanisms still apply.

Idempotency does not guarantee availability

A perfectly idempotent service can still be unavailable.

CreateOrder

safe to repeat

does not mean:

CreateOrder

will eventually succeed

The database may remain down.

The request may exceed its deadline.

The service may reject the operation for a business reason.

Idempotency changes the risk of repetition.

It does not change every failure into success.

This distinction matters because retries should still be:

bounded

delayed

deadline-aware

limited during overload

Safe repetition can still create excessive traffic.

Idempotency does not guarantee exactly-once delivery

A broker may deliver an event twice.

An idempotent consumer can produce one local effect.

deliveries:
    2

business effect:
    1

That is different from exactly-once delivery.

The consumer received both attempts.

It neutralised the duplicate at the application boundary.

This is often the more useful guarantee:

Delivery may be repeated, but the protected business effect is not multiplied.

The boundary should always be named.

exactly one Kafka output record?

exactly one database row?

exactly one payment capture?

exactly one email?

exactly one order?

Different effects may require different mechanisms.

Idempotency must include downstream calls

Suppose order handles an idempotent PlaceOrder command.

It protects order creation using:

checkout_id:
    checkout-7f82

Inside the operation, it calls payment without an idempotency identity.

PLACE ORDER

idempotent locally


PAYMENT CAPTURE

not idempotent

A retry can preserve one order while creating two captures.

The complete operation is only as safe as its non-idempotent side effects.

The service should propagate or derive stable identities for downstream commands.

checkout_id:
    checkout-7f82

payment_attempt_id:
    payment-91

Payment then enforces:

one capture for payment-91

Idempotency does not automatically flow through the call graph.

Each side-effecting boundary needs its own repeatability contract.

Sending email is not naturally idempotent

An email provider may accept the same message twice.

The customer receives two emails.

The notification service can create one durable delivery job:

notification_id:
    order-5001 / ORDER_CONFIRMED

Repeated event handling maps to the same job.

event delivery 1
        │
        ▼
create notification job


event delivery 2
        │
        ▼
job already exists

The worker then sends the job.

If the provider accepts the email but the response is lost, the worker may still be uncertain.

Possible protections include:

provider idempotency key

provider message lookup

stable Message-ID header

local sent-state reconciliation

careful retry policy

A unique local job prevents duplicate job creation.

It does not prove that an external provider produced exactly one delivery.

The protected boundary must extend far enough to cover the effect that matters.

Payments make the stakes visible

Suppose payment capture uses:

capture_id:
    capture-91

The provider records:

capture-91 → provider transaction 8821

Repeated requests with the same capture ID return the original result.

attempt 1:
    capture succeeds
    response lost

attempt 2:
    same capture_id
    provider returns transaction 8821

Without this identity:

attempt 1:
    transaction 8821

attempt 2:
    transaction 8822

The difference is not merely technical cleanliness.

It is charging the customer once or twice.

Payment systems make idempotency easy to respect because the duplicate effect is expensive and visible.

The same principle applies to less dramatic effects:

one order

one reservation

one refund

one shipment

one loyalty award

one notification

A possible bfstore checkout design

Suppose the storefront begins checkout.

It creates:

checkout_id:
    checkout-7f82

It sends:

message PlaceOrderRequest {
  string checkout_id = 1;
  string basket_id = 2;
  string customer_id = 3;
}

Order uses checkout_id as a business uniqueness key.

ORDERS

checkout_id:
    unique

The first attempt begins a transaction:

BEGIN
  │
  ├── reserve checkout-7f82 operation
  ├── create order-5001
  ├── record checkout-7f82 → order-5001
  └── record OrderCreated in outbox
COMMIT

The response is lost.

The storefront retries:

PlaceOrder

checkout_id:
    checkout-7f82

Order reads the existing result:

checkout-7f82:
    completed

order:
    order-5001

and returns it.

The workflow then requests inventory:

ReserveStock

reservation_id:
    reservation-71

order_id:
    order-5001

Inventory enforces:

reservation_id:
    unique

Repeated attempts return the same reservation.

Payment receives:

AuthorisePayment

payment_attempt_id:
    payment-91

order_id:
    order-5001

Payment and its provider use payment-91 as the idempotency identity.

When order publishes:

OrderConfirmed

event_id:
    event-44

notification records:

notification:
    order-5001 / ORDER_CONFIRMED

as a unique effect.

ONE CHECKOUT
    │
    ├── one order
    ├── one stock reservation
    ├── one payment attempt
    └── one confirmation notification

Each component owns its own idempotency boundary.

The correlation ID connects the workflow.

The operation IDs protect the individual effects.

A practical idempotency record

For a service operation, a record might contain:

operation key:
    checkout-7f82

operation type:
    PlaceOrder

caller:
    storefront

request fingerprint:
    8b91...

status:
    completed

resource:
    order-5001

created:
    2022-11-13T14:30:01Z

completed:
    2022-11-13T14:30:01Z

expires:
    2022-11-20T14:30:01Z

Possible statuses include:

received

processing

completed

rejected

failed

outcome_unknown

The status model should answer:

Can another attempt begin work?

Should it return a previous result?

Should it wait?

Does reconciliation need to run?

May the operation be attempted again
under a new identity?

The table should not become a decorative archive.

It is part of the operation’s concurrency and recovery model.

An idempotency checklist

Logical operation

What real-world intention do repeated attempts represent?

Operation identity

Which stable key identifies that intention?

Key creator

Which caller knows when a new intention begins?

Scope

Is the key unique per customer, tenant,
operation type or globally?

Request consistency

What happens when the same key arrives
with different request data?

Acquisition

How does one attempt atomically become
the owner of a new operation?

Concurrency

Can two simultaneous attempts both begin work?

Atomicity

Are the business effect and idempotency result
committed together?

In-progress state

What does another attempt receive while
the first is still running?

Response

Does a duplicate return the original result,
current resource state or an operation status?

Downstream effects

Do payment, messaging and external providers
receive their own stable operation identities?

Consumer effects

Can message redelivery repeat local work?

Business uniqueness

Can a database constraint protect the real effect?

Retention

How long will duplicate protection remain available?

Replay

How is authorised recovery distinguished
from an accidental duplicate?

Reconciliation

How is an uncertain external outcome discovered?

Observability

Can operators see new attempts, duplicate attempts,
key conflicts and reused results separately?

Security

Can one caller guess another caller's key
and retrieve or interfere with its result?

Privacy

Does the stored request or response contain
data that requires limited retention or access?

Idempotency is not one header added to an endpoint.

It is a relationship between identity, storage, concurrency, side effects and recovery.

The mental model I am keeping

My original model was:

request fails
      │
      ▼
retry request
      │
      ▼
hope the first attempt did nothing

The stronger model is:

                     LOGICAL OPERATION
                              │
                              ▼
                       OPERATION IDENTITY
                              │
             ┌────────────────┼────────────────┐
             │                │                │
             ▼                ▼                ▼
         attempt 1        attempt 2        attempt 3
             │                │                │
             └────────────────┼────────────────┘
                              ▼
                    IDEMPOTENCY BOUNDARY
                              │
              ┌───────────────┴───────────────┐
              │                               │
              ▼                               ▼
        operation absent                 operation known
              │                               │
              ▼                               ▼
       perform once and                 return or resume
       record outcome                   known operation
              │                               │
              └───────────────┬───────────────┘
                              ▼
                    ONE BUSINESS EFFECT

The idempotency key answers:

Do these attempts belong to the same intention?

The unique constraint answers:

Which attempt is allowed to create the effect?

The durable record answers:

What outcome did that intention produce?

The request fingerprint answers:

Is the caller reusing the identity consistently?

The retention policy answers:

How long can the service recognise the operation?

Reconciliation answers:

What happened when the stored result remains uncertain?

And downstream idempotency answers:

Did the entire operation remain singular,
or only the first database row?

Retries are valuable because distributed failures are often temporary.

Retries are dangerous because the earlier attempt may already have succeeded.

Idempotency connects those truths.

It allows the system to say:

Yes, I have seen this intention before.

No, I will not create another effect.

Here is the result I already know,
or the operation state you can safely observe.

That is what makes trying again different from doing it again.

References and further reading

RFC 7231: HTTP/1.1 Semantics and Content Defines safe and idempotent HTTP methods and explains why an idempotent request can be repeated after a communication failure.

Amazon Builders’ Library: Making retries safe with idempotent APIs Explains caller-provided request identifiers, duplicate detection, late-arriving requests and the distinction between repeated attempts and new intentions.

Amazon Builders’ Library: Timeouts, retries, and backoff with jitter Discusses retry safety, side effects, overload and the need to design APIs for repeated attempts.

Stripe: Idempotent requests Documents the use of idempotency keys for safely repeating API requests and returning the previously recorded result.

Google Cloud APIs: Request identification Describes using request identifiers to prevent duplicate execution of mutating operations.

Chris Richardson: Idempotent Consumer Describes message consumers that record processed message identities or otherwise make repeated delivery safe.

Chris Richardson: Transactional Outbox Explains committing business state and an outbound message record in one database transaction so publication retries do not repeat the original business operation.