¿Qué es la idempotencia?
La idempotencia es la propiedad de que una operación produce el mismo resultado sin importar cuántas veces se ejecute. Es un concepto importante para garantizar la fiabilidad en sistemas distribuidos y diseño de APIs.
Definición matemática: f(f(x)) = f(x) Aplicar la misma función múltiples veces produce el mismo resultado que aplicarla una vez
¿Por qué es importante la idempotencia?
En las comunicaciones de red, pueden ocurrir los siguientes problemas.
sequenceDiagram
participant C as Cliente
participant S as Servidor
C->>S: Solicitud
Note over C,S: Timeout<br/>(La respuesta no llegó)
Note over C: ¿Se procesó la solicitud?<br/>¿Debería reintentar?
Si la operación es idempotente, se puede reintentar de forma segura.
Métodos HTTP e idempotencia
| Método | Idempotente | Descripción |
|---|---|---|
| GET | ✓ | Obtención de recursos, sin efectos secundarios |
| HEAD | ✓ | Igual que GET (sin cuerpo) |
| PUT | ✓ | Reemplazo completo del recurso |
| DELETE | ✓ | Eliminación del recurso |
| POST | ✗ | Creación de recursos, con efectos secundarios |
| PATCH | ✗ | Actualización parcial (depende de la implementación) |
PUT vs POST
| Método | Resultado |
|---|---|
| PUT /users/123 | Sin importar cuántas veces se ejecute, el usuario 123 queda en el mismo estado |
| POST /users | Cada ejecución puede crear un nuevo usuario |
Clave de idempotencia (Idempotency Key)
Es una técnica para lograr idempotencia incluso en solicitudes POST.
POST /payments
Idempotency-Key: pay_abc123xyz
Content-Type: application/json
{
"amount": 5000,
"currency": "JPY"
}
Ejemplo de implementación
async function processPayment(req, res) {
const idempotencyKey = req.headers['idempotency-key'];
if (!idempotencyKey) {
return res.status(400).json({ error: 'Idempotency-Key required' });
}
// Verificar resultado de procesamiento existente
const existing = await redis.get(`idempotency:${idempotencyKey}`);
if (existing) {
return res.status(200).json(JSON.parse(existing));
}
// Adquirir bloqueo (prevenir ejecución simultánea)
const lock = await acquireLock(idempotencyKey);
if (!lock) {
return res.status(409).json({ error: 'Request in progress' });
}
try {
// Ejecutar el procesamiento
const result = await executePayment(req.body);
// Guardar el resultado (válido por 24 horas)
await redis.setex(
`idempotency:${idempotencyKey}`,
86400,
JSON.stringify(result)
);
return res.status(201).json(result);
} finally {
await releaseLock(idempotencyKey);
}
}
Patrones de implementación de idempotencia
1. Verificación de duplicados mediante identificador único
async function createOrder(orderData, requestId) {
// Verificar existencia
const existing = await db.orders.findByRequestId(requestId);
if (existing) {
return existing; // Devolver el mismo resultado
}
// Nueva creación
const order = await db.orders.create({
...orderData,
requestId // Guardar el identificador único
});
return order;
}
2. Verificación de estado
async function cancelOrder(orderId) {
const order = await db.orders.findById(orderId);
// Si ya está cancelado, no hacer nada
if (order.status === 'cancelled') {
return order; // Idempotente
}
// Verificar si se puede cancelar
if (order.status === 'shipped') {
throw new Error('Cannot cancel shipped order');
}
return await db.orders.update(orderId, { status: 'cancelled' });
}
3. Bloqueo optimista
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');
}
}
Hacer idempotentes las operaciones no idempotentes
Operaciones de incremento
// No idempotente
UPDATE balance SET amount = amount + 100 WHERE user_id = 123
// Hacer idempotente (establecer valor absoluto)
UPDATE balance SET amount = 5100 WHERE user_id = 123
// O, gestionar con ID de transacción
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;
Envío de correo electrónico
async function sendWelcomeEmail(userId, requestId) {
// Verificar si ya se envió
const sent = await db.emailLogs.findOne({
userId,
type: 'welcome',
requestId
});
if (sent) {
return { status: 'already_sent' };
}
// Enviar
await emailService.send(/* ... */);
// Registrar en el log
await db.emailLogs.create({
userId,
type: 'welcome',
requestId,
sentAt: new Date()
});
return { status: 'sent' };
}
Estrategias de reintento
Backoff exponencial
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;
// Backoff exponencial + jitter
const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
await sleep(delay);
}
}
}
Errores que permiten reintento
| Reintento | Tipo |
|---|---|
| ✓ Posible | Errores 5xx (error del servidor), timeout, errores de red temporales |
| ✗ No posible | Errores 4xx (error del cliente), errores de lógica de negocio, errores de autenticación |
Resumen
La idempotencia es un principio de diseño importante para garantizar la fiabilidad de sistemas distribuidos y APIs. Utiliza patrones como la introducción de claves de idempotencia, verificación de estado y bloqueo optimista para diseñar operaciones que puedan reintentarse de forma segura.
← Volver a la lista