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

  1. Client generates a unique token: Usually a UUID (Universally Unique Identifier)
  2. Client includes token in request: The token is sent with the operation request
  3. Server checks token: Before processing, the server checks if this token has been seen before
  4. 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.