Operations that produce the same result when applied multiple times, critical for reliable distributed systems with retries and duplicate message handling
70% of API design interviews
Powers systems at Payment systems, order processing
Safe retries query improvement
At-least-once + idempotence
TL;DR
Idempotent operations produce the same result regardless of how many times theyβre executed. In distributed systems, idempotence enables safe retries, duplicate message handling, and exactly-once semantics without complex deduplication logic. Critical for building reliable APIs and message processing systems.
Visual Overview
NON-IDEMPOTENT OPERATION (Dangerous with retries)
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β Operation: account.balance += 100 (INCREMENT) β
β β
β Execution 1: balance = 1000 + 100 = 1100 β β
β Network fails, client retries... β
β Execution 2: balance = 1100 + 100 = 1200 β β
β (WRONG! User charged twice) β
β β
β Problem: Multiple executions β different β
β results (unintended side effects) β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
IDEMPOTENT OPERATION (Safe with retries)
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β Operation: account.balance = 1100 (SET) β
β β
β Execution 1: balance = 1100 β β
β Network fails, client retries... β
β Execution 2: balance = 1100 β β
β (Same result! Safe) β
β β
β Property: Multiple executions β same result β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
IDEMPOTENT WITH REQUEST ID (Best Practice)
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β Request: { β
β request_id: "txn_abc123", β
β action: "deposit", β
β amount: 100 β
β } β
β β β
β Execution 1: β
β - Check: request_id processed? NO β
β - Execute: balance += 100 β 1100 β
β - Store: request_id = "txn_abc123" β
β - Return: success β β
β β β
β Network fails, client retries... β
β β β
β Execution 2: β
β - Check: request_id processed? YES β
β - Skip execution (already done) β
β - Return: success β (same result) β
β β
β Result: Safe retries, no duplicate processing β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
HTTP METHOD IDEMPOTENCE
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β GET /users/123 β
β β Idempotent β (read, no side effects) β
β β
β PUT /users/123 {"name": "Alice"} β
β β Idempotent β (set to specific value) β
β β
β DELETE /users/123 β
β β Idempotent β (deleted or already deleted) β
β β
β POST /users {"name": "Bob"} β
β β NOT Idempotent β (creates new resource) β
β β
β POST /orders/123/pay β
β β NOT Idempotent β (charges money) β
β Unless: Use idempotency key β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
Core Explanation
What is Idempotence?
Idempotence (from mathematics) means an operation produces the same result when applied multiple times:
f(f(x)) = f(x)
Examples:
- SET value = 42: Idempotent (repeated SET has same effect)
- INCREMENT value: NOT idempotent (repeated INCREMENT increases value)
- DELETE item: Idempotent (item deleted, stays deleted)
- CREATE item: NOT idempotent (creates duplicate items)
In Distributed Systems:
Idempotence enables safe retries when network failures or timeouts occur, eliminating the need to distinguish between:
- Request failed (retry needed)
- Request succeeded but response lost (retry causes duplicate)
Why Idempotence Matters
Problem: Network Timeouts
Scenario: Payment API call
Client sends: "Charge $100 to card"
β
Network timeout (no response received)
β
Did payment succeed or fail?
Without idempotence:
- Don't retry β User may not be charged (bad)
- Retry β User may be charged twice (worse!)
With idempotence:
- Retry safely β Guaranteed charged exactly once β
At-Least-Once + Idempotence = Exactly-Once
Message delivery guarantees:
At-Most-Once:
- Message delivered 0 or 1 times
- May lose messages
- Use case: Metrics (OK to lose)
At-Least-Once:
- Message delivered 1+ times
- No loss, but duplicates possible
- Use case: Most systems
Exactly-Once:
- Message delivered exactly 1 time
- Hard to implement (requires transactions)
- OR: At-least-once + idempotent processing β
Idempotent vs Non-Idempotent Operations
Naturally Idempotent Operations:
SET operations:
account.balance = 1100 β Idempotent
user.email = "alice@example.com" β Idempotent
DELETE operations:
DELETE FROM users WHERE id=123 β Idempotent
(second delete: already gone)
Absolute updates:
UPDATE users SET status='active' β Idempotent
WHERE id=123
Read operations:
SELECT * FROM users WHERE id=123 β Idempotent
(no side effects)
NOT Idempotent (Require Special Handling):
INCREMENT operations:
account.balance += 100 β Not idempotent
view_count++ β Not idempotent
CREATE operations:
INSERT INTO orders (id, amount) β Not idempotent
(creates duplicate rows)
Relative updates:
UPDATE users SET age = age + 1 β Not idempotent
Implementing Idempotence
1. Unique Request IDs (Idempotency Keys)
API Request with idempotency key:
POST /api/payments
Headers:
Idempotency-Key: req_abc123xyz
Body:
{
"amount": 100,
"currency": "USD",
"card_id": "card_789"
}
Server-side implementation:
ββββββββββββββββββββββββββββββββββββββββββββββ
β 1. Extract idempotency key from header β
β 2. Check if key exists in database: β
β - EXISTS: Return cached response β β
β - NOT EXISTS: Process request β
β 3. Execute business logic β
β 4. Store key + response in database β
β 5. Return response β
ββββββββββββββββββββββββββββββββββββββββββββββ
Result:
- First request (key=req_abc123xyz): Process payment
- Retry (same key): Return cached result, no duplicate charge
- New request (key=req_def456uvw): Process new payment
2. Natural Idempotency (Design for It)
BAD (Non-Idempotent):
POST /orders
{
"product": "laptop",
"quantity": 1
}
β Creates new order each time β
GOOD (Idempotent with client-generated ID):
PUT /orders/order_abc123
{
"product": "laptop",
"quantity": 1
}
β Creates or updates order_abc123 β
β Retry-safe
GOOD (Idempotent with unique constraint):
POST /orders
{
"client_order_id": "order_abc123", // Unique!
"product": "laptop",
"quantity": 1
}
β Database unique constraint prevents duplicates β
3. Database-Level Idempotency
Using database constraints:
CREATE TABLE payments (
id SERIAL PRIMARY KEY,
request_id VARCHAR(255) UNIQUE, -- Idempotency key
amount DECIMAL(10, 2),
status VARCHAR(50),
created_at TIMESTAMP
);
Application code:
INSERT INTO payments (request_id, amount, status)
VALUES ('req_abc123', 100.00, 'completed')
ON CONFLICT (request_id) DO NOTHING;
Result:
- First insert: Creates payment
- Retry: Conflict detected, no duplicate payment β
4. State Machine Approach
Payment state machine:
States: PENDING β PROCESSING β COMPLETED
β
FAILED
Transitions are idempotent:
- PENDING β PROCESSING: OK
- PROCESSING β PROCESSING: OK (retry, same state)
- PROCESSING β COMPLETED: OK
- COMPLETED β COMPLETED: OK (already completed)
Implementation:
UPDATE payments
SET status = 'COMPLETED'
WHERE id = 123 AND status IN ('PENDING', 'PROCESSING')
β Retrying "complete payment" is safe β
Idempotent Message Processing
Kafka Consumer Example:
Problem: Kafka guarantees at-least-once delivery
β Messages may be processed multiple times
Solution: Idempotent consumer
Non-Idempotent Consumer (Bad):
ββββββββββββββββββββββββββββββββββββββββββββββ
β consume message: "increment counter" β
β counter++ // β Not idempotent β
β commit offset β
β β
β If crash before commit: β
β β Reprocess message β
β β counter++ again (duplicate) β
ββββββββββββββββββββββββββββββββββββββββββββββ
Idempotent Consumer (Good):
ββββββββββββββββββββββββββββββββββββββββββββββ
β consume message: { β
β message_id: "msg_123", β
β action: "increment_counter" β
β } β
β β β
β if not processed(message_id): β
β counter++ β
β mark_processed(message_id) β
β β β
β commit offset β
β β
β If crash and reprocess: β
β β Check: msg_123 processed? YES β
β β Skip increment β β
ββββββββββββββββββββββββββββββββββββββββββββββ
Common Patterns
1. Stripe API Style
POST /v1/charges
Headers:
Idempotency-Key: unique_key_here
Body:
{
"amount": 2000,
"currency": "usd"
}
Behavior:
- First request: Create charge, return 200
- Retry with same key: Return cached 200 (no new charge)
- Different key: Create new charge
- Key expires after 24 hours
2. AWS S3 Style
PUT /bucket/object.txt
Content: "Hello World"
Behavior:
- Uploading same object multiple times: Idempotent β
- Result always: object.txt contains "Hello World"
- Uses content-based addressing (ETag)
3. Database Upsert Style
INSERT INTO users (id, name, email)
VALUES (123, 'Alice', 'alice@example.com')
ON DUPLICATE KEY UPDATE
name = VALUES(name),
email = VALUES(email)
Behavior:
- First call: Insert new user
- Retry: Update user (same result)
- Idempotent β
Real Systems Using Idempotence
System | Idempotency Mechanism | Key Feature | Use Case |
---|---|---|---|
Stripe API | Idempotency-Key header | 24-hour key expiration | Payment processing |
AWS APIs | Client request token | Service-specific | CloudFormation, EC2 |
Kafka | Message offset + deduplication | Consumer-side | Stream processing |
Kubernetes | Declarative desired state | Reconciliation loop | Container orchestration |
HTTP PUT | Resource URI | REST semantics | RESTful APIs |
Git | Content-addressable | SHA hashes | Version control |
Case Study: Stripe Payments
Stripe Idempotency Implementation:
POST https://api.stripe.com/v1/charges
Headers:
Authorization: Bearer sk_test_...
Idempotency-Key: req_abc123
Body:
amount=2000¤cy=usd
First Request:
1. Server checks: key "req_abc123" exists? NO
2. Process payment β charge card
3. Store: {key: "req_abc123", response: {...}, ttl: 24h}
4. Return: 200 OK {id: "ch_789", amount: 2000}
Retry (network timeout):
1. Server checks: key "req_abc123" exists? YES
2. Fetch cached response
3. Return: 200 OK {id: "ch_789", amount: 2000}
(Same charge ID, no duplicate payment)
Different Request:
Idempotency-Key: req_def456
β New payment, different charge ID
Key Expiration:
- Keys expire after 24 hours
- After expiration, same key creates new charge
Case Study: Kafka Idempotent Producer
Kafka Producer Idempotence (since 0.11):
Properties config = new Properties();
config.put("enable.idempotence", "true");
config.put("acks", "all");
config.put("retries", Integer.MAX_VALUE);
How it works:
ββββββββββββββββββββββββββββββββββββββββββββββ
β Producer assigns sequence numbers: β
β Message 1: {seq: 0, data: "msg1"} β
β Message 2: {seq: 1, data: "msg2"} β
β Message 3: {seq: 2, data: "msg3"} β
β β β
β Broker tracks: producer_id + seq number β
β β β
β If duplicate received: β
β - Message seq=1 already written β
β - Discard duplicate, ACK success β β
β β β
β Result: Exactly-once delivery to topic β
ββββββββββββββββββββββββββββββββββββββββββββββ
Guarantees:
β No duplicate messages in partition
β Messages ordered within partition
β Safe retries (producer can retry forever)
When to Use Idempotence
β Perfect Use Cases
Payment Processing
Scenario: Credit card charges
Requirement: Never double-charge users
Solution: Idempotency keys for payment API
Benefit: Safe retries on network failures
Order Processing
Scenario: E-commerce order placement
Requirement: Same order submitted multiple times β single order
Solution: Client-generated order ID
Benefit: Prevent duplicate orders
Inventory Updates
Scenario: Deduct inventory on purchase
Requirement: Don't deduct twice on retry
Solution: Transaction ID + database constraint
Benefit: Accurate inventory counts
Message Processing
Scenario: Kafka consumer processing events
Requirement: Process each message exactly once
Solution: Message ID tracking
Benefit: At-least-once + idempotence = exactly-once
β When NOT to Use (or Use Carefully)
Intentional Duplicates
Example: User clicking "Add to Cart" multiple times
Intent: Add multiple items
Solution: Don't use idempotency for this use case
Time-Sensitive Operations
Example: Stock trading (buy at current price)
Problem: Price changes between retries
Solution: Idempotency key + timestamp validation
Analytics/Metrics
Example: Page view counters
Acceptable: Slight overcounting on retries
Alternative: Use approximate counters (HyperLogLog)
Interview Application
Common Interview Question
Q: βDesign an API for a payment system. How would you handle network retries to prevent double-charging users?β
Strong Answer:
βIβd implement idempotent payment processing using idempotency keys:
API Design:
POST /api/v1/payments Headers: Authorization: Bearer token Idempotency-Key: unique_request_id Body: { "amount": 100.00, "currency": "USD", "payment_method_id": "pm_123" }
Server-Side Implementation:
Extract Idempotency Key:
- Required header, client-generated UUID
- Example:
Idempotency-Key: req_a1b2c3d4
Check Idempotency Table:
CREATE TABLE idempotency_keys ( key VARCHAR(255) PRIMARY KEY, request_hash VARCHAR(255), response_status INT, response_body TEXT, created_at TIMESTAMP, INDEX idx_created (created_at) );
Processing Logic:
BEGIN TRANSACTION SELECT * FROM idempotency_keys WHERE key = :key IF EXISTS: // Validate request unchanged (hash matches) IF request_hash matches: RETURN cached response β ELSE: RETURN 400 Bad Request (key reused with different request) ELSE: // First time seeing this key // Process payment charge = stripe.charges.create(...) // Store idempotency record INSERT INTO idempotency_keys ( key, request_hash, response_status, response_body ) VALUES (:key, :hash, 200, :response) COMMIT TRANSACTION RETURN 200 OK {charge_id: ...}
Benefits:
- Safe Retries: Client can retry infinitely
- No Double-Charging: Same key β same result
- Request Validation: Hash ensures request unchanged
Key Management:
- TTL: Expire keys after 24 hours
- Cleanup: Periodic job removes old keys
- Monitoring: Alert on high duplicate rate
Edge Cases:
- Concurrent Requests (same key):
- Use database locking (SELECT FOR UPDATE)
- First request processes, others wait
- All return same result
- Partial Failures:
- Payment succeeded but idempotency insert failed
- Solution: Store idempotency key in payment record
- Recovery: Lookup by key in payments table
- Key Reuse (malicious or accidental):
- Validate request hash matches
- Return 400 if different request with same key
Alternatives Considered:
- No idempotency: Unacceptable (double-charging risk)
- Request deduplication only: Insufficient (response needed)
- Distributed lock: More complex, chose DB-based approach
Real-World Example: Stripe uses this exact pattern with Idempotency-Key headerβ
Code Example
Idempotent Payment API
from flask import Flask, request, jsonify
import hashlib
import json
import uuid
from datetime import datetime, timedelta
app = Flask(__name__)
# Simple in-memory store (use database in production)
idempotency_store = {}
payments_store = {}
def compute_request_hash(request_data):
"""Compute hash of request body for validation"""
return hashlib.sha256(
json.dumps(request_data, sort_keys=True).encode()
).hexdigest()
@app.route('/api/v1/payments', methods=['POST'])
def create_payment():
"""Idempotent payment endpoint"""
# Extract idempotency key
idempotency_key = request.headers.get('Idempotency-Key')
if not idempotency_key:
return jsonify({'error': 'Idempotency-Key header required'}), 400
# Get request data
request_data = request.get_json()
request_hash = compute_request_hash(request_data)
# Check if we've seen this idempotency key before
if idempotency_key in idempotency_store:
stored = idempotency_store[idempotency_key]
# Validate request unchanged
if stored['request_hash'] != request_hash:
return jsonify({
'error': 'Idempotency key reused with different request'
}), 400
# Return cached response
print(f"β Returning cached response for key: {idempotency_key}")
return jsonify(stored['response']), stored['status_code']
# First time seeing this key - process payment
try:
# Simulate payment processing
payment_id = str(uuid.uuid4())
# Create payment record
payment = {
'id': payment_id,
'amount': request_data['amount'],
'currency': request_data.get('currency', 'USD'),
'status': 'succeeded',
'created_at': datetime.now().isoformat()
}
payments_store[payment_id] = payment
# Store idempotency record
idempotency_store[idempotency_key] = {
'request_hash': request_hash,
'response': payment,
'status_code': 200,
'created_at': datetime.now()
}
print(f"β Processed payment: {payment_id} for key: {idempotency_key}")
return jsonify(payment), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/v1/payments/<payment_id>', methods=['GET'])
def get_payment(payment_id):
"""Retrieve payment (idempotent by nature)"""
payment = payments_store.get(payment_id)
if not payment:
return jsonify({'error': 'Payment not found'}), 404
return jsonify(payment), 200
# Cleanup old idempotency keys (run periodically)
def cleanup_old_keys():
"""Remove idempotency keys older than 24 hours"""
cutoff = datetime.now() - timedelta(hours=24)
keys_to_remove = [
key for key, value in idempotency_store.items()
if value['created_at'] < cutoff
]
for key in keys_to_remove:
del idempotency_store[key]
print(f"Cleaned up {len(keys_to_remove)} old idempotency keys")
if __name__ == '__main__':
# Example usage:
# curl -X POST http://localhost:5000/api/v1/payments \
# -H "Content-Type: application/json" \
# -H "Idempotency-Key: req_abc123" \
# -d '{"amount": 100.00, "currency": "USD"}'
app.run(debug=True, port=5000)
Idempotent Kafka Consumer
import json
from kafka import KafkaConsumer
class IdempotentConsumer:
"""Kafka consumer with idempotent message processing"""
def __init__(self, topic, processed_messages_store):
self.consumer = KafkaConsumer(
topic,
bootstrap_servers=['localhost:9092'],
enable_auto_commit=False, # Manual commit after processing
value_deserializer=lambda m: json.loads(m.decode('utf-8'))
)
self.processed_messages = processed_messages_store
def process_messages(self):
"""Process messages idempotently"""
for message in self.consumer:
# Extract message ID (required for idempotence)
msg_data = message.value
message_id = msg_data.get('message_id')
if not message_id:
print("Warning: Message without ID, processing anyway")
self._process_message(msg_data)
self.consumer.commit()
continue
# Check if already processed
if message_id in self.processed_messages:
print(f"β Message {message_id} already processed, skipping")
self.consumer.commit() # Commit offset to avoid reprocessing
continue
# Process message
try:
self._process_message(msg_data)
# Mark as processed
self.processed_messages.add(message_id)
# Commit offset (atomic with marking processed)
self.consumer.commit()
print(f"β Processed message {message_id}")
except Exception as e:
print(f"Error processing message {message_id}: {e}")
# Don't commit - will retry on restart
def _process_message(self, msg_data):
"""Business logic (can be non-idempotent internally)"""
# Example: Increment counter (non-idempotent operation)
# But overall flow is idempotent due to message ID tracking
action = msg_data.get('action')
if action == 'increment_counter':
counter_name = msg_data['counter']
# Increment counter...
print(f"Incrementing counter: {counter_name}")
elif action == 'send_email':
recipient = msg_data['recipient']
# Send email...
print(f"Sending email to: {recipient}")
# Usage
processed_messages = set() # In production: Use Redis/DB
consumer = IdempotentConsumer('events', processed_messages)
consumer.process_messages()
Related Content
Prerequisites:
- Distributed Systems Basics - Foundation concepts
Related Concepts:
- Exactly-Once Semantics - Delivery guarantees
- Offset Management - Kafka consumer tracking
- Producer Acknowledgments - Message durability
Used In Systems:
- Stripe API: Idempotency keys for payments
- Kafka: Idempotent producer and consumer patterns
- REST APIs: HTTP PUT/DELETE idempotent semantics
Explained In Detail:
- Distributed Systems Deep Dive - Idempotence patterns
Quick Self-Check
- Can explain idempotence in 60 seconds?
- Know difference between idempotent and non-idempotent operations?
- Understand how idempotency keys work?
- Can implement idempotent API endpoint?
- Know how at-least-once + idempotence = exactly-once?
- Can design idempotent message processing?