Concepto, funcionamiento e implementacion práctica
La idempotencia es un concepto crucial en sistemas distribuidos y servicios web, ya que garantiza que múltiples solicitudes idénticas tengan el mismo efecto que una sola. Este patrón es especialmente útil en escenarios como el procesamiento de pagos, la creación de pedidos y otras operaciones donde las solicitudes duplicadas pueden tener consecuencias imprevistas.
El patrón de idempotencia es un patrón arquitectónico utilizado para garantizar que una misma operación ejecutada repetidas veces produce siempre el mismo resultado, sin efectos secundarios adicionales.
En palabras simples:
- Idempotencia = aunque llames varias veces, solo se procesa una vez...
Ejemplos típicos:
- Evitar que un pago se procese dos veces si el usuario presiona el botón Pagar repetidamente.
- Evitar que un microservicio ejecute dos veces la misma transacción cuando un cliente reintenta.
- Evitar duplicados cuando hay fallos de red o timeouts.
En microservicios, especialmente usando WebFlux / Reactor / WebClient / AWS / colas / eventos:
- Hay reintentos automáticos.
- Hay duplicados inevitables.
- Hay timeouts.
- La red NO es confiable. (fallos, latencias, reconexiones)
Sin idempotencia, una operación crítica puede ejecutarse N veces.
- Problemas que soluciona la idempotencia
- Doble débito en un pago.
- Duplicación de órdenes.
- Reprocesamiento de eventos.
- Creación de registros repetidos.
- Inconsistencias en bases distribuidas.
El patrón se basa en 4 conceptos:
-
Idempotency-Key (clave única por operación)
- El cliente envía en el header: Idempotency-Key: 11111111-aaaa-bbbb-cccc-000000000001
- Esa clave identifica la intención del cliente.
-
Registro de la operación en estado IN_PROGRESS
- Cuando llega una solicitud nueva:
- Se verifica si el key existe.
- Si NO existe → se crea un registro:
- state = IN_PROGRESS
- requestHash =
- Y se guarda en Redis + DynamoDB.
- Cuando llega una solicitud nueva:
-
Se ejecuta la operación SOLO si somos el owner
Si nadie más está ejecutando este mismo Idempotency-Key, el servicio ejecuta el caso de uso.
-
Se guarda el resultado FINAL
-
Cuando termina:
- state = COMPLETED
- responseJson = { ... }
- httpStatus = 200
-
Si falló:
- state = FAILED
- responseJson = { error }
-
El servicio revisa:
- Si ya existe:
- COMPLETED → devuelve la MISMA respuesta.
- FAILED → devuelve el MISMO error.
- IN_PROGRESS → devuelve 409 → “Transacción en progreso”
- NO vuelve a ejecutar la operación física.
-
Cliente presiona “Pagar”
-
El móvil reintenta 3 veces por mala señal.
-
El servidor recibe 3 requests con el mismo Idempotency-Key.
- Resultado:
- El débito solo ocurre 1 vez.
- Las 3 solicitudes reciben la misma respuesta.
- No hay doble cobro.
- No hay reprocesamiento.
- No hay efectos secundarios adicionales.
- Resultado:
- Redis: respuesta rápida, TTL corto.
- DynamoDB: fuente de verdad, condición attribute_not_exists(pk).
- Hash del request: evita reuso con payload diferente.
- Estados:
- IN_PROGRESS
- COMPLETED
- FAILED
- Cache final del resultado.
- Thread-safety distribuido.
- Detección de concurrencia. (solo 1 owner)
El siguiente diagrama de secuencia ilustra el flujo de gestión de solicitudes idempotentes mediante una combinación de Redis para el almacenamiento en caché de ruta rápida y DynamoDB para el almacenamiento duradero. Este patrón garantiza que las solicitudes repetidas con la misma clave de idempotencia produzcan resultados consistentes sin efectos secundarios no deseados.
sequenceDiagram
autonumber
participant C as Client
participant H as Handler
participant F as IdempotencyFilter
participant S as IdempotencyService
participant R as Redis
participant D as DynamoDB
participant U as UseCase (Action)
Note over C: Request with Idempotency-Key
C->>H: POST /transactions<br/>headers: Idempotency-Key
H->>F: apply(request, body, action, type)
F->>S: execute(idemKey, body, action, type)
Note over S: 1. Fast-path Redis lookup
S->>R: GET idem:{key}
alt Redis HIT (COMPLETED/FAILED/IN_PROGRESS)
R-->>S: record
S->>S: resolveFromExisting()
alt COMPLETED
S-->>F: return cached response
F-->>H: response
H-->>C: 200 OK (idempotent)
else FAILED
S-->>F: throw BusinessException
F-->>H: error
H-->>C: HTTP error cached
else IN_PROGRESS
S-->>F: IDEMPOTENCY_IN_PROGRESS
F-->>H: 409 Conflict
H-->>C: 409 Conflict
end
else Redis MISS
Note over S: 2. Try insert IN_PROGRESS in Dynamo
S->>D: PutItem IF attribute_not_exists(pk)
alt Dynamo CREATED (owner)
D-->>S: OK
Note over S: sync IN_PROGRESS to Redis
S->>R: SET IN_PROGRESS<br/>TTL short
R-->>S: OK
Note over S: 3. Execute action
S->>U: process(body)
U-->>S: response
Note over S: 4. Save COMPLETED
S->>D: UpdateItem<br/>state=COMPLETED,<br/>responseJson,httpStatus
S->>R: SET COMPLETED<br/>TTL long
S-->>F: response
F-->>H: response
H-->>C: 200 OK (first execution)
else Dynamo EXISTS (not owner)
D-->>S: ConditionalCheckFailed
Note over S: Read truth from Dynamo
S->>D: GetItem
D-->>S: existing record
S->>R: SET cache copy
R-->>S: OK
S->>S: resolveFromExisting()
alt COMPLETED exists
S-->>F: return existing response
F-->>H: cached response
H-->>C: 200 OK (idempotent)
else FAILED exists
S-->>F: throw cached error
F-->>H: error
H-->>C: cached error
else still IN_PROGRESS
S-->>F: IDEMPOTENCY_IN_PROGRESS
F-->>H: 409
H-->>C: 409 Conflict
end
end
end
- postman (dir): Contiene archivos de colección y entorno de Postman para probar el patrón de idempotencia.
- docs:md: Este archivo README con una explicación detallada y un diagrama de secuencia.
Author: Raul Bolivar Navas
This project is licensed under the MIT License - see the LICENSE file for details.