Imagina esta clásica pesadilla del comercio electrónico: un cliente viaja en el metro con una conexión 4G irregular. Pulsa «Pagar 2.500 TL». La solicitud llega a tu backend y se cobra su tarjeta, pero la respuesta se pierde antes de llegar a su teléfono. Frustrado, pulsa el botón dos veces más, o su cliente HTTP reintenta la solicitud automáticamente.
Diez segundos después, recibe tres SMS de su banco: le cobraron tres veces la tarjeta de crédito por exactamente el mismo pedido.
1. Por qué «basta con deshabilitar el botón en React» falla en producción
Los desarrolladores junior suelen responder a este escenario diciendo: «¡Fácil! Solo pon disabled={isLoading} en el botón de envío en el frontend».
Aunque deshabilitar el botón es buena UX, nunca garantiza la seguridad del backend:
- Reintentos a nivel de red: las apps móviles, los API gateways y los emisores de webhooks reintentan las solicitudes automáticamente cuando se agota el tiempo de espera de una conexión TCP.
- Solicitudes desde varias pestañas o dispositivos: un usuario puede abrir el checkout en dos pestañas o saltarse por completo las restricciones de la interfaz del frontend.
- El problema de los Dos Generales: cuando una conexión de red se corta a mitad de camino, el cliente no tiene forma de saber si el servidor falló antes de cobrar la tarjeta o después de cobrarla.
Para resolver esto de forma segura, la operación de pago debe ser idempotente.
2. ¿Qué es la idempotencia (y una Idempotency Key)?
En matemáticas y en informática, una operación es idempotente si ejecutarla una vez produce exactamente el mismo estado del sistema que ejecutarla diez veces.
GET /orders/123yDELETE /orders/123son idempotentes por naturaleza.POST /payments/chargeNO es idempotente por naturaleza: cada llamada crea un cobro nuevo.
Para hacer idempotente POST /payments/charge, el cliente genera una Idempotency Key única (normalmente un UUIDv4 asociado a esa intención de checkout concreta) y la envía en la cabecera HTTP:
POST /api/v1/payments/charge HTTP/1.1
Host: api.czay.dev
Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
Content-Type: application/json
{
"orderId": "ord_8841",
"amount": 250000,
"currency": "TRY"
}3. Implementación paso a paso en el backend (Redis + base de datos)
Cuando tu backend recibe una solicitud con una Idempotency-Key, ejecuta una máquina de estados atómica y estricta:
export async function processPayment(req, res) {
const key = req.headers['idempotency-key'];
if (!key) return res.status(400).json({ error: 'Missing Idempotency-Key header' });
const redisKey = `idempotency:${req.user.id}:${key}`;
// 1. Atomic Lock (SET if Not Exists with 24h TTL)
const acquired = await redis.set(redisKey, JSON.stringify({ status: 'IN_PROGRESS' }), 'EX', 86400, 'NX');
if (!acquired) {
const existing = JSON.parse(await redis.get(redisKey));
// If the first request is still talking to the bank right now:
if (existing.status === 'IN_PROGRESS') {
return res.status(409).json({ error: 'Payment is currently being processed. Please wait.' });
}
// If the first request already succeeded, return the cached receipt without charging again!
return res.status(existing.statusCode).json(existing.body);
}
try {
// 2. Execute the actual payment gateway call (iyzico / PayTR / Stripe)
const paymentResult = await paymentGateway.charge(req.body);
// 3. Cache the final response under the Idempotency Key
const responsePayload = { status: 'COMPLETED', statusCode: 200, body: paymentResult };
await redis.set(redisKey, JSON.stringify(responsePayload), 'EX', 86400);
return res.status(200).json(paymentResult);
} catch (err) {
// If a transient network error occurred before reaching the bank, release the lock so retry is allowed
await redis.del(redisKey);
throw err;
}
}Información: Respalda siempre Redis con una restricción única en la base de datos
Además del lock rápido de Redis (SET NX), añade siempre un índice UNIQUE sobre idempotency_key (o sobre order_id para los cobros completados) en tu tabla payments de PostgreSQL como red de seguridad ACID definitiva.
Hoja de ruta y herramienta interactivas
System Design: escalar a un millón de usuarios
Añadir servidores no basta: un mapa de System Design, etapa por etapa, de por qué los sistemas se caen en un día grande y cómo se recorta la carga antes de que llegue a la base de datos.
¿SQL o NoSQL?
Responde seis preguntas y te diré qué tipo de base de datos encaja con tu proyecto, y por qué.
Sigue leyendo
- CZES¿Qué es Dokku? Reduce una factura cloud de 200 $ a un VPS de 5 $ con una arquitectura PaaS de código abierto
¿Qué es Dokku? Reduce una factura cloud de 200 $ a un VPS de 5 $ con una arquitectura PaaS de código abierto
25 de octubre de 2026 · 5 min de lectura - CZESCortando el ruido: ¿qué lenguaje y framework de backend deberías elegir en 2026?
Cortando el ruido: ¿qué lenguaje y framework de backend deberías elegir en 2026?
25 de octubre de 2026 · 4 min de lectura