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

AlmacenamientoVentajasDesventajas
Base de datosPersistente, transaccionalMás lento, requiere limpieza
Redis/CacheRápido, TTL automáticoPuede perder datos si se reinicia
Tabla dedicadaSeparación clara, fácil de consultarTabla 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íaOpciones
Almacenamiento de keysRedis (con TTL), PostgreSQL, DynamoDB
Headers estándarIdempotency-Key (Stripe), X-Request-Id, If-Match (ETags)
LibreríasStripe SDK (idempotencia integrada), custom middleware
Brokers con dedupKafka (exactly-once semantics), SQS (deduplication ID)
FrameworksExpress 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.