Idempotencia
Cómo diseñar operaciones que pueden ejecutarse múltiples veces sin efectos secundarios duplicados, esencial para reintentos seguros y procesamiento confiable de eventos.
Qué problema resuelve
En sistemas distribuidos, los mensajes y las solicitudes pueden llegar más de una vez por diversas razones:
- El cliente reintenta una solicitud porque no recibió respuesta (pero el servidor sí la procesó)
- El broker de mensajería entrega un evento duplicado (garantía at-least-once)
- Un fallo de red causa que la confirmación se pierda, y el emisor reenvía
- El usuario hace doble clic en el botón de “Pagar”
Sin idempotencia:
Cliente ──► POST /payments {amount: 100}
✅ Pago procesado (pero respuesta se pierde)
Cliente ──► POST /payments {amount: 100} (reintento)
✅ Pago procesado OTRA VEZ
Resultado: Se cobraron $200 en lugar de $100
Sin idempotencia, los reintentos y los duplicados causan efectos secundarios repetidos: cobros dobles, inventario descontado dos veces, emails duplicados, registros duplicados en la base de datos.
Cómo funciona
Una operación es idempotente cuando ejecutarla una vez produce el mismo resultado que ejecutarla múltiples veces. El sistema detecta solicitudes duplicadas y devuelve el resultado original sin volver a ejecutar la operación.
Con idempotencia:
Cliente ──► POST /payments
Idempotency-Key: "PAY-abc-123"
{amount: 100}
✅ Pago procesado → respuesta se pierde
Cliente ──► POST /payments
Idempotency-Key: "PAY-abc-123" (mismo key)
{amount: 100}
✅ Devuelve resultado anterior (sin procesar de nuevo)
Resultado: Se cobró $100 una sola vez
Mecanismo con Idempotency Key
El enfoque más común es usar una clave de idempotencia (idempotency key) que identifica de forma única cada operación:
1. Cliente genera un ID único: "PAY-abc-123"
2. Cliente envía request con Idempotency-Key header
3. Servidor verifica si ya procesó esa key:
a. Si NO existe → procesa la operación, guarda resultado con la key
b. Si SÍ existe → devuelve el resultado guardado sin reprocesar
Almacenamiento de keys
| Almacenamiento | Ventajas | Desventajas |
|---|---|---|
| Base de datos | Persistente, transaccional | Más lento, requiere limpieza |
| Redis/Cache | Rápido, TTL automático | Puede perder datos si se reinicia |
| Tabla dedicada | Separación clara, fácil de consultar | Tabla adicional que mantener |
Ejemplo de flujo completo
Tabla: idempotency_keys
┌──────────────────┬────────┬──────────────────┬─────────┐
│ key │ status │ response │ expires │
├──────────────────┼────────┼──────────────────┼─────────┤
│ PAY-abc-123 │ done │ {paymentId: 456} │ 24h │
│ PAY-def-456 │ doing │ null │ 24h │
└──────────────────┴────────┴──────────────────┴─────────┘
Request 1: POST /payments (key: PAY-abc-123)
→ Key no existe → INSERT key con status "doing"
→ Procesar pago → UPDATE key con status "done" y response
→ Devolver response
Request 2: POST /payments (key: PAY-abc-123) [reintento]
→ Key existe con status "done"
→ Devolver response guardada (sin reprocesar)
Request 3: POST /payments (key: PAY-def-456)
→ Key existe con status "doing" (en proceso)
→ Devolver HTTP 409 Conflict (espere y reintente)
Idempotencia en consumidores de eventos
Para consumidores de eventos, la idempotencia se implementa rastreando qué eventos ya fueron procesados:
Evento recibido: OrderCreated (eventId: "EVT-789")
1. Verificar si eventId ya fue procesado
→ SELECT * FROM processed_events WHERE event_id = 'EVT-789'
2a. Si NO existe:
→ Procesar evento (reservar inventario)
→ INSERT INTO processed_events (event_id, processed_at)
→ ACK al broker
2b. Si SÍ existe:
→ Ignorar (ya fue procesado)
→ ACK al broker
Operaciones naturalmente idempotentes
Algunas operaciones son idempotentes por naturaleza y no necesitan mecanismos adicionales:
✅ Naturalmente idempotentes:
PUT /users/123 {name: "María"} → Siempre deja el mismo estado
DELETE /orders/456 → Borrar algo ya borrado = no-op
GET /products/789 → Lectura, sin efectos secundarios
❌ NO idempotentes (necesitan mecanismo):
POST /payments {amount: 100} → Cada ejecución crea un pago nuevo
POST /orders {items: [...]} → Cada ejecución crea un pedido nuevo
POST /notifications {msg: "Hola"} → Cada ejecución envía un mensaje
Ventajas
- Reintentos seguros: Los clientes pueden reintentar sin miedo a efectos duplicados
- Procesamiento confiable de eventos: Los consumidores manejan duplicados correctamente
- Experiencia de usuario: El doble clic no causa problemas
- Consistencia de datos: No hay registros duplicados ni cobros dobles
- Simplifica la resiliencia: El patrón Retry funciona correctamente cuando el downstream es idempotente
- Auditoría: El registro de idempotency keys sirve como log de operaciones
Trade-offs / Desventajas
- Almacenamiento adicional: Necesitas guardar las keys y sus resultados
- Complejidad de implementación: Manejar estados concurrentes (doing/done) requiere cuidado
- Limpieza de keys: Las keys antiguas deben limpiarse periódicamente (TTL)
- Generación de keys: El cliente debe generar keys únicas y consistentes
- Latencia: La verificación de duplicados añade una consulta extra por operación
- Consistencia transaccional: La key y la operación deben guardarse en la misma transacción
Cuándo usar
- Operaciones de pago y transacciones financieras
- Cualquier endpoint que reciba reintentos (POST, PATCH con efectos secundarios)
- Consumidores de eventos en arquitecturas event-driven
- Operaciones que se disparan por webhooks (que pueden llegar duplicados)
- Cualquier operación donde un duplicado cause daño al negocio
- APIs públicas donde no controlas el comportamiento del cliente
Cuándo evitar
- Operaciones de solo lectura (GET) que ya son idempotentes
- Operaciones PUT/DELETE que son naturalmente idempotentes
- Sistemas internos con comunicación confiable y sin reintentos
- Prototipos donde la complejidad adicional no se justifica
Tecnologías e implementaciones comunes
| Categoría | Opciones |
|---|---|
| Almacenamiento de keys | Redis (con TTL), PostgreSQL, DynamoDB |
| Headers estándar | Idempotency-Key (Stripe), X-Request-Id, If-Match (ETags) |
| Librerías | Stripe SDK (idempotencia integrada), custom middleware |
| Brokers con dedup | Kafka (exactly-once semantics), SQS (deduplication ID) |
| Frameworks | Express middleware, Spring interceptor, ASP.NET filter |
Relación con otros patrones
- Retry: Los reintentos son seguros solo si el downstream es idempotente
- Event-Driven Architecture: Los consumidores de eventos deben ser idempotentes (at-least-once delivery)
- Outbox Pattern: Garantiza publicación de eventos, pero el consumidor aún necesita idempotencia
- Saga Pattern: Cada paso de la saga debe ser idempotente para manejar reintentos de compensación
- Circuit Breaker: Cuando el circuito se cierra y se reintentan operaciones, la idempotencia evita duplicados
Próximos pasos
La idempotencia es un requisito fundamental para sistemas distribuidos confiables. Para entender cómo compartir modelos de forma controlada entre servicios, explora el Shared Kernel. Para ver cómo la idempotencia se aplica en transacciones distribuidas, revisa el Saga Pattern.