Skip to content

Idempotence

8 min Intermediate Patterns Interview: 70%

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
Interview Relevance
70% of API design interviews
🏭 Payment systems, order processing
Production Impact
Powers systems at Payment systems, order processing
⚑ Safe retries
Performance
Safe retries query improvement
πŸ“ˆ At-least-once + idempotence
Scalability
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

SystemIdempotency MechanismKey FeatureUse Case
Stripe APIIdempotency-Key header24-hour key expirationPayment processing
AWS APIsClient request tokenService-specificCloudFormation, EC2
KafkaMessage offset + deduplicationConsumer-sideStream processing
KubernetesDeclarative desired stateReconciliation loopContainer orchestration
HTTP PUTResource URIREST semanticsRESTful APIs
GitContent-addressableSHA hashesVersion 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&currency=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:

  1. Extract Idempotency Key:

    • Required header, client-generated UUID
    • Example: Idempotency-Key: req_a1b2c3d4
  2. 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)
    );
    
  3. 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:

  1. Concurrent Requests (same key):
    • Use database locking (SELECT FOR UPDATE)
    • First request processes, others wait
    • All return same result
  2. Partial Failures:
    • Payment succeeded but idempotency insert failed
    • Solution: Store idempotency key in payment record
    • Recovery: Lookup by key in payments table
  3. 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()

Prerequisites:

Related Concepts:

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?