- Author: Amin Boulouma, Software Engineer
- Github source code: https://github.com/aminblm/ai_systems_design_from_scratch
- Engineering Blog: https://aminblm.github.io/ai_systems_design_from_scratch/blog/
The Idempotency Infrastructure: Safe Retries at Scale
In distributed systems, network packets go missing, and services hang. The natural response to a failed request is to retry. But what happens if the first request actually succeeded, and it was only the acknowledgment that failed? You perform the operation twice. This causes the “midnight deployment spike” of duplicates: double payments, double orders, or corrupted ledger entries.
Idempotency is the property where an operation can be performed multiple times without changing the result beyond the initial application. Implementing an idempotency infrastructure is the primary defense against the chaos of network unreliability.
The Theory: The Idempotency Key
The core of this architecture is the Idempotency Key. Every request is assigned a unique identifier by the client. The server tracks these keys. If a request arrives with a key that has already been processed, the server returns the cached result of the original operation instead of re-executing the logic.
Glossary for Beginners
- Idempotent: A fancy word for “doing it twice is the same as doing it once.” (Like pressing an elevator button; pressing it five times doesn’t make the elevator come faster).
- Network Partition: When two servers stop being able to talk to each other.
- Race Condition: When two requests arrive at the exact same time and fight to update the same piece of data.
- Cache: A temporary storage where we remember the result of a previous job so we don’t have to do it again.
Simple Implementation: The Key Validator
This example uses a dictionary to “remember” keys that have already been processed.
class IdempotentProcessor:
def __init__(self):
self.processed_keys = set()
def process(self, key, operation_func):
if key in self.processed_keys:
return "Already processed"
result = operation_func()
self.processed_keys.add(key)
return result
Complex Implementation: Production-Grade Infrastructure
In production, keys must be stored in a distributed database (like Redis) with a Time-To-Live (TTL) to prevent your memory from filling up with old keys.
import time
class IdempotencyLayer:
def __init__(self, storage_backend):
self.db = storage_backend # e.g., Redis
def execute(self, key, task):
# 1. Atomic check and set
if self.db.exists(key):
return self.db.get(key)
# 2. Execute the task
result = task()
# 3. Store result with TTL (e.g., 24 hours)
self.db.set(key, result, ttl=86400)
return result
Quick Reference: Handling Duplicates
| Strategy | Pros | Cons |
|---|---|---|
| Idempotency Keys | Safest, standard for APIs | Requires client-side support |
| Optimistic Locking | Handles concurrent updates | Can cause high collision rates |
| Unique Constraints | Database-level protection | Doesn’t work for complex business logic |
Why We Choose Idempotency over Blind Retries
We choose Idempotency because it transforms an inherently “unreliable” network into a “reliable” distributed system. It allows us to safely retry requests after timeouts, network errors, or crashes without fear of side effects. Without it, you are effectively betting that your network will never drop a packet—a bet you will eventually lose.
Developer Checklist
- Are your API endpoints requiring an
Idempotency-Keyheader? - Is your idempotency storage backend distributed (e.g., Redis cluster)?
- Does your logic handle the “In-Flight” state (e.g., what if a request is currently processing)?
- Is your TTL long enough to cover typical retry windows?
Takeaways
- Design for Failure: Assume every request will be sent at least twice.
- Persistence: Idempotency keys must outlive the individual request lifecycle.
- Atomicity: The check-and-set operation must be atomic to prevent race conditions.