Operations that produce the same result when applied multiple times, critical for reliable distributed systems with retries and duplicate message handling
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
Idempotence Overview
Idempotence 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: Saferetries, 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:
Idempotence Definition
Idempotence Definition
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
Network Timeout Problem
Network Timeout Problem
Scenario: Payment API call
Clientsends: "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
Message Delivery Guarantees
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 deliveredexactly 1 time
- Hard to implement (requires transactions)
- OR: At-least-once + idempotent processing✓
Idempotent vs Non-Idempotent Operations
Naturally Idempotent Operations:
Naturally Idempotent Operations
Naturally Idempotent Operations
SET operations:
account.balance = 1100 ✓Idempotent
user.email = "alice@example.com" ✓IdempotentDELETE 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):
Non-Idempotent Operations
Non-Idempotent Operations
INCREMENT operations:
account.balance += 100 ✕ Not idempotent
view_count++ ✕ Not idempotentCREATE 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)
Idempotency Keys Implementation
Idempotency Keys Implementation
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: Returncached 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)
Natural Idempotency Patterns
Natural Idempotency Patterns
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-safeGOOD (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
Database-Level Idempotency
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
State Machine Idempotency
State Machine Idempotency
Payment state machine:
States: PENDING → PROCESSING → COMPLETED
↓FAILEDTransitions 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:
Idempotent Kafka Consumer
Idempotent Kafka Consumer
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
Stripe API Idempotency
Stripe API Idempotency
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: Returncached 200 (no new charge)
- Different key: Create new charge
- Key expires after 24 hours
2. AWS S3 Style
AWS S3 Idempotency
AWS S3 Idempotency
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
Database Upsert Idempotency
Database Upsert Idempotency
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
Stripe Idempotency Implementation
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 Idempotent Producer
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 ││ - Discardduplicate, 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
Payment Processing Use Case
Payment Processing Use Case
Scenario: Credit card charges
Requirement: Never double-charge users
Solution: Idempotency keys for payment API
Benefit: Safe retries on network failures
Order Processing
Order Processing Use Case
Order Processing Use Case
Scenario: E-commerce order placement
Requirement: Same order submitted multiple times →single orderSolution: Client-generated order ID
Benefit: Preventduplicate orders
Inventory Updates
Inventory Updates Use Case
Inventory Updates Use Case
Scenario: Deduct inventory on purchase
Requirement: Don't deduct twice on retry
Solution: Transaction ID + database constraint
Benefit: Accurate inventory counts
Message Processing
Message Processing Use Case
Message Processing Use Case
Scenario: Kafka consumer processing events
Requirement: Process each message exactly onceSolution: Message ID tracking
Benefit: At-least-once + idempotence = exactly-once
✕ When NOT to Use (or Use Carefully)
Intentional Duplicates
Intentional Duplicates Warning
Intentional Duplicates Warning
Example: User clicking "Add to Cart" multiple times
Intent: Add multiple itemsSolution: Don't use idempotency for this use case
Time-Sensitive Operations
Time-Sensitive Operations Warning
Time-Sensitive Operations Warning
Example: Stock trading (buy at current price)
Problem: Price changes between retries
Solution: Idempotency key + timestamp validation
Analytics/Metrics
Analytics/Metrics Warning
Analytics/Metrics Warning
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:
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: ...}