What is Idempotency
Idempotency is the property where executing the same operation multiple times produces the same result. It is an important concept for ensuring reliability in distributed systems and API design.
Mathematical Definition: f(f(x)) = f(x) Applying the same function multiple times yields the same result as applying it once.
Why Idempotency Matters
In network communication, the following problems can occur:
sequenceDiagram
participant C as Client
participant S as Server
C->>S: Request
Note over C,S: Timeout (Response didn't arrive)
Note over C: Question: Was the request processed?<br/>Should we retry?
If an operation is idempotent, it can be safely retried.
HTTP Methods and Idempotency
| Method | Idempotent | Description |
|---|---|---|
| GET | ✓ | Resource retrieval, no side effects |
| HEAD | ✓ | Same as GET (no body) |
| PUT | ✓ | Complete replacement of resource |
| DELETE | ✓ | Resource deletion |
| POST | ✗ | Resource creation, has side effects |
| PATCH | ✗ | Partial update (depends on implementation) |
PUT vs POST
| Method | Result |
|---|---|
| PUT /users/123 | No matter how many times executed, user 123 is in the same state |
| POST /users | A new user might be created each time it’s executed |
Idempotency Key
A technique to achieve idempotency even with POST requests.
POST /payments
Idempotency-Key: pay_abc123xyz
Content-Type: application/json
{
"amount": 5000,
"currency": "JPY"
}
Implementation Example
async function processPayment(req, res) {
const idempotencyKey = req.headers['idempotency-key'];
if (!idempotencyKey) {
return res.status(400).json({ error: 'Idempotency-Key required' });
}
// Check for existing processing result
const existing = await redis.get(`idempotency:${idempotencyKey}`);
if (existing) {
return res.status(200).json(JSON.parse(existing));
}
// Acquire lock (prevent concurrent execution)
const lock = await acquireLock(idempotencyKey);
if (!lock) {
return res.status(409).json({ error: 'Request in progress' });
}
try {
// Execute processing
const result = await executePayment(req.body);
// Save result (valid for 24 hours)
await redis.setex(
`idempotency:${idempotencyKey}`,
86400,
JSON.stringify(result)
);
return res.status(201).json(result);
} finally {
await releaseLock(idempotencyKey);
}
}
Idempotency Implementation Patterns
1. Duplicate Check with Unique Identifier
async function createOrder(orderData, requestId) {
// Check for existing
const existing = await db.orders.findByRequestId(requestId);
if (existing) {
return existing; // Return same result
}
// Create new
const order = await db.orders.create({
...orderData,
requestId // Save unique identifier
});
return order;
}
2. State Check
async function cancelOrder(orderId) {
const order = await db.orders.findById(orderId);
// Do nothing if already cancelled
if (order.status === 'cancelled') {
return order; // Idempotent
}
// Check if cancellation is possible
if (order.status === 'shipped') {
throw new Error('Cannot cancel shipped order');
}
return await db.orders.update(orderId, { status: 'cancelled' });
}
3. Optimistic Locking
async function updateInventory(productId, quantity, version) {
const result = await db.query(
`UPDATE inventory
SET quantity = quantity - $1, version = version + 1
WHERE product_id = $2 AND version = $3`,
[quantity, productId, version]
);
if (result.rowCount === 0) {
throw new Error('Concurrent modification detected');
}
}
Making Non-Idempotent Operations Idempotent
Increment Operations
// Non-idempotent
UPDATE balance SET amount = amount + 100 WHERE user_id = 123
// Make it idempotent (set absolute value)
UPDATE balance SET amount = 5100 WHERE user_id = 123
// Or, manage with transaction ID
INSERT INTO transactions (id, user_id, amount)
VALUES ('tx_abc', 123, 100)
ON CONFLICT (id) DO NOTHING;
UPDATE balance
SET amount = (SELECT SUM(amount) FROM transactions WHERE user_id = 123)
WHERE user_id = 123;
Email Sending
async function sendWelcomeEmail(userId, requestId) {
// Check if already sent
const sent = await db.emailLogs.findOne({
userId,
type: 'welcome',
requestId
});
if (sent) {
return { status: 'already_sent' };
}
// Send
await emailService.send(/* ... */);
// Record log
await db.emailLogs.create({
userId,
type: 'welcome',
requestId,
sentAt: new Date()
});
return { status: 'sent' };
}
Retry Strategies
Exponential Backoff
async function retryWithBackoff(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
// Exponential backoff + jitter
const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
await sleep(delay);
}
}
}
Errors to Retry
| Retriable | Non-retriable |
|---|---|
| 5xx errors (server errors) | 4xx errors (client errors) |
| Timeouts | Business logic errors |
| Temporary network errors | Authentication errors |
Summary
Idempotency is an important design principle for ensuring reliability in distributed systems and APIs. Use patterns like idempotency keys, state checks, and optimistic locking to design operations that can be safely retried.
← Back to list