Back to Blog
Lesson 27 of the System Design: System Design Fundamentals course
ArchitectureAugust 13, 20264 min read

Idempotency in Distributed Systems: Building Reliable APIs

Learn how to design idempotent API endpoints using unique request IDs and database constraints to ensure system reliability even when network retries occur.

idempotencydistributed systemsarchitectureapireliability
Complex network of tangled power lines and cables in Chiang Mai, Thailand.

Previously in this course, we explored designing for failure and implementing circuit breakers. These strategies ensure your system remains resilient during outages, but they often trigger retries. This lesson adds the critical safety layer needed to handle those retries: idempotency.

The Problem: When "Once" Becomes "Twice"

In distributed systems, network partitions are inevitable. A client might send a POST /payments request, the server might process it successfully, but the network connection drops before the client receives the 200 OK. The client, assuming the request failed, retries.

Without idempotency, you process the payment twice. Idempotency is the property where an operation can be applied multiple times without changing the result beyond the initial application. Achieving this is non-negotiable for any state-changing operation in a robust architecture.

Designing Idempotent API Endpoints

To design idempotent endpoints, we move away from "blind" execution. Instead, we implement a "Check-Act" pattern driven by a unique Idempotency Key.

The flow works like this:

  1. The client generates a unique ID (UUID) for the specific request.
  2. The client sends this ID in a custom header, typically Idempotency-Key.
  3. The server checks if this key has already been processed.
  4. If yes, the server returns the cached result of the previous operation.
  5. If no, the server processes the request and stores the result associated with that key.

Worked Example: Protecting a Payment Endpoint

Let’s implement this using a simple Redis store to track keys and a relational database for the state.

PYTHON
import uuid
from flask import request, jsonify
import redis

# Simple cache client
cache = redis.Redis(host=CE9178">'localhost', port=6379, db=0)

def process_payment():
    key = request.headers.get(CE9178">'Idempotency-Key')
    if not key:
        return jsonify({"error": "Missing Idempotency-Key"}), 400

    # 1. Check if we've seen this key
    cached_response = cache.get(f"idempotency:{key}")
    if cached_response:
        return cached_response, 200

    # 2. Process the business logic
    # In a real app, use a database transaction here
    result = execute_payment_logic(request.json)

    # 3. Cache the result for future retries
    cache.set(f"idempotency:{key}", result, ex=3600) # Expire after 1 hour
    
    return jsonify(result), 200

Handling Duplicate Event Processing

When dealing with message brokers—which we introduced in our lesson on handling background tasks—the "at-least-once" delivery guarantee means duplicates are guaranteed.

To handle this, the consumer must be idempotent:

  • Database Unique Constraints: If you are inserting records, rely on a UNIQUE constraint on the business key (e.g., transaction_id). The database will reject a duplicate insertion, which you can catch and gracefully ignore.
  • State Machines: Ensure your entity state only moves forward. If an order is already SHIPPED, a duplicate MARK_AS_SHIPPED event should be treated as a no-op rather than an error.

Hands-on Exercise

For our running project, we are currently designing the payment microservice.

  1. Update your API design document to include an Idempotency-Key requirement for all POST and PATCH endpoints.
  2. Sketch out a database schema change: add a table named processed_requests with columns idempotency_key (PK) and response_payload.
  3. Briefly explain how this table prevents the "double payment" scenario described above.

Common Pitfalls

  • Ignoring Expiration: If you store idempotency keys forever, your database or cache will explode in size. Always set a TTL (Time-To-Live) on your idempotency keys.
  • Non-Deterministic Keys: Never use a timestamp or a generic value as an idempotency key. Always use a UUID generated on the client side before the first attempt.
  • Ignoring Concurrency: If two identical requests hit the server at the exact same time, both might see the cache as empty. Use a distributed lock or a database INSERT ... ON CONFLICT to ensure atomic processing.

FAQ

Q: Do I need idempotency for GET requests? A: No. HTTP GET requests are idempotent by definition—they should not have side effects.

Q: How long should I store idempotency keys? A: Typically 24 hours is sufficient, as most retry logic happens within minutes.

Q: Is idempotency the same as transactionality? A: No, but they are cousins. Transactionality ensures the operation is atomic; idempotency ensures the operation is safe to repeat.

Recap

Achieving idempotency is the difference between a system that breaks under pressure and one that gracefully handles network flakiness. By enforcing unique request IDs and utilizing database constraints, you ensure that your system remains consistent regardless of how many times a client retries an operation. Implementing these patterns, as discussed in REST API Idempotency and the Redis Idempotency Pattern, is a fundamental skill for building professional, reliable distributed systems.

Up next: We will explore Service Discovery to help our microservices find each other in a dynamic environment.

Similar Posts