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.