When communication between systems there is no guarantee that requests will only be send / received only once. Idempotency ensures that performing the same operation multiple times has the same effect as performing it once, preventing duplicate processing and data corruption. This is crucial for building reliable systems that can handle the inevitable failures and retries that occur in distributed environments.
What is Idempotency? Link to heading
An operation is idempotent if performing it multiple times has the same effect as performing it once. This means that no matter how many times you execute the same operation with the same parameters, the end result should be identical.
Consider a simple example: setting a user’s email address to “john@example.com”. Whether you do this once or a hundred times, the result is the same—the user’s email is “john@example.com”. This is an idempotent operation.
However, not all operations are naturally idempotent. Adding £100 to a bank account is not idempotent—doing it twice results in £200 being added instead of £100.
Why Idempotency Matters Link to heading
In distributed systems, failures are inevitable for more information please read my articles on the 8 fallacies of distributed systems. Networks can drop packets, messages can be delivered more than once, services can crash, and timeouts can occur. When these failures happen, clients often retry operations, which can lead to unintended consequences if those operations aren’t idempotent.
The Banking Problem Link to heading
Imagine a banking system where a client wants to transfer £10,000 from their account to another account. The client sends a request to the banking API, but due to a network timeout, they don’t receive a response. Unsure whether the transfer was successful, the client retries the request.
Without proper idempotency handling, this scenario could result in:
- First request: Successfully transfers £10,000
- Retry request: Transfers another £10,000 (total: £20,000 transferred)
This is clearly not what the client intended, and it could have serious financial consequences.
Implementing Idempotency with Tokens Link to heading
The solution is to use idempotency tokens—unique identifiers that clients provide with their requests to ensure that duplicate requests can be identified and handled appropriately.
How Idempotency Tokens Work Link to heading
- Client generates a unique token: Usually a UUID (Universally Unique Identifier)
- Client includes token in request: The token is sent with the operation request
- Server checks token: Before processing, the server checks if this token has been seen before
- Server responds appropriately:
- If token is new: Process the operation and store the result
- If token exists: Return the previously stored result without reprocessing
Important Considerations Link to heading
Idempotency tokens should NOT be used as identifiers. They are purely for preventing duplicate processing and should be only considered unique inside of a business appropriate scope (Usually user or account). when creating a resource (Payment Request / resource / ect. ) the globally unique external identifier should always be created by the system that owns the domain and never by an external party as lead to an information leakage attack vector.
For example, in our banking transfer:
- Idempotency Token:
550e8400-e29b-41d4-a716-446655440000(prevents duplicate processing) - Business Identifier:
TRANSFER-12345(the globally unique transfer reference for external use)
Practical Implementation Link to heading
Client-Side Implementation Link to heading
import uuid
from dataclasses import dataclass
from decimal import Decimal
@dataclass
class TransferRequest:
idempotency_token: str = None
from_account: str = None
to_account: str = None
amount: Decimal = None
reference: str = None
def __post_init__(self):
if self.idempotency_token is None:
self.idempotency_token = str(uuid.uuid4())
Server-Side Implementation Link to heading
import asyncio
from typing import Optional
async def process_transfer(request: TransferRequest) -> TransferResult:
# Check if we've already processed this idempotency token
existing_result = await idempotency_store.get(request.idempotency_token)
if existing_result is not None:
return existing_result # Return cached result
# Process the transfer
result = await banking_service.transfer(
request.from_account,
request.to_account,
request.amount,
request.reference
)
# Store the result for future duplicate requests
await idempotency_store.store(request.idempotency_token, result)
return result
Best Practices Link to heading
Token Generation Link to heading
- Use UUIDs: They provide sufficient uniqueness for most use cases
- Client responsibility: The client should generate the token, not the server
Token Storage Link to heading
- Store results, not just tokens: Cache the actual response for the token
- Set expiration: Idempotency tokens shouldn’t live forever—set reasonable TTLs (Time to Live) if using an idempotency store so they can be cleaned up
- Consider storage costs: Use appropriate storage solutions based on your scale
Error Handling Link to heading
- Distinguish between retries and new requests: Use different tokens for genuinely new requests
- Handle partial failures: If processing fails after storing the token, you may need cleanup mechanisms
- Provide clear responses: Return appropriate HTTP status codes (201 (Resource Created) for the first time a request is processed successful and a 200 (Request Successful) for each time after that)
Common Patterns Link to heading
RESTful APIs Link to heading
POST /api/transfers
Content-Type: application/json
{
"idempotency_token": "550e8400-e29b-41d4-a716-446655440000",
"fromAccount": "12345678",
"toAccount": "87654321",
"amount": 10000.00,
"reference": "TRANSFER-12345"
}
Testing Idempotency Link to heading
Testing idempotency is crucial to ensure your implementation works correctly:
import pytest
@pytest.mark.asyncio
async def test_transfer_with_same_idempotency_token_should_not_duplicate_transfer():
# Arrange
idempotency_token = str(uuid.uuid4())
request = TransferRequest(
idempotency_token=idempotency_token,
from_account="12345678",
to_account="87654321",
amount=Decimal("10000.00")
)
# Act - Send the same request twice
result1 = await transfer_service.process_transfer(request)
result2 = await transfer_service.process_transfer(request)
# Assert
assert result1.transfer_id == result2.transfer_id
assert banking_service.transfer_call_count == 1
When Not to Use Idempotency Link to heading
While idempotency is powerful, it’s not always appropriate:
- Read operations: GET requests are naturally idempotent
- Operations with side effects: If you need to track every attempt (like audit logs), idempotency might not be suitable
- Time-sensitive operations: Some operations might need to be executed every time, even if they appear duplicate
Conclusion Link to heading
Idempotency is a fundamental concept in building resilient systems. By using idempotency tokens correctly—as tools for preventing duplicate processing rather than as business identifiers—you can ensure that your systems handle failures and retries gracefully.
Remember: in distributed systems, operations will be retried. Design your systems to handle this reality, and you’ll build more reliable, fault-tolerant applications that can survive the inevitable failures that occur in distributed environments.
The key is to make idempotency a first-class citizen in your API design, not an afterthought. Your users—and your bank account—will thank you for it.