Saltar al contenido
dumaloor.dev
~/academia
Fundamentos

Por qué a veces se cobra dos veces: la idempotencia contada con un datáfono

El cliente pulsa pagar, no pasa nada, vuelve a pulsar. Ahí se decide si tu sistema cobra una vez o dos, y eso depende de una decisión técnica que se toma antes.

6 min de lecturapagosidempotenciaintegracionesfiabilidad

El datáfono pide la tarjeta. Pasan tres segundos, cinco, ocho. La pantalla sigue igual, sin confirmar nada. El cliente, dudando si ha ido bien, vuelve a acercar la tarjeta por si acaso. Esta vez sí, el datáfono confirma el pago al momento. Todos respiran. Y al día siguiente, mirando el extracto, aparecen dos cargos idénticos donde debería haber uno solo.

Nadie ha hecho nada mal a propósito. El cliente hizo lo razonable —esperar, dudar, reintentar—. El datáfono hizo lo razonable —cobrar cuando recibió una orden de cobro—. El problema es que, por dentro, ese instante de duda generó dos órdenes de cobro idénticas, una detrás de otra, y nada en el sistema sabía que eran la misma intención repetida y no dos compras reales.

La escena se repite, con pequeñas variaciones, en cualquier negocio que cobra online o en persona: la venta de entradas de una sala de conciertos en la noche de mayor tráfico, la barra de un evento en el momento de más cola, el formulario de reserva de un restaurante con dos locales un sábado por la noche. Cuanta más gente comprando a la vez, más veces se repite ese instante exacto de duda, y más veces se dispara, sin que nadie lo busque, la misma petición dos veces.

#Lo que ha pasado por dentro

Cuando se pulsa «pagar» y la pantalla tarda en responder, lo más habitual no es que no haya pasado nada: es que la petición de cobro sí ha salido, pero la confirmación de que ha ido bien tarda en volver o se pierde por el camino. Desde el punto de vista de quien mira la pantalla, parece que no ha pasado nada, así que vuelve a pulsar. Por dentro, sin embargo, han salido dos peticiones de cobro seguidas, con los mismos datos, la misma tarjeta y el mismo importe, separadas por unos segundos.

Esto no pasa solo con datáfonos físicos. Pasa exactamente igual en cualquier pago por internet: un formulario que tarda en responder, un usuario que pulsa «confirmar» dos veces por impaciencia, una conexión que se corta un instante justo después de enviar el pago. El mecanismo de fondo es siempre el mismo: dos peticiones idénticas, disparadas con segundos de diferencia, porque quien esperaba una respuesta no la vio llegar a tiempo.

#Por qué el sistema no puede distinguirlo por sí solo

«Que el sistema se dé cuenta» parece la solución evidente, y conviene entender por qué no lo es. Dos peticiones de cobro idénticas —misma tarjeta, mismo importe, segundos de diferencia— pueden ser dos intentos del mismo pago que falló al confirmar. Pero también pueden ser dos compras reales y distintas: la misma persona comprando dos entradas iguales, dos consumiciones seguidas en la barra de un evento, dos pedidos idénticos hechos a propósito. Sin más información, el sistema no tiene forma de saber, por sí solo, en cuál de los dos casos está.

Es exactamente el mismo problema que ya aparece cuando llega repetido un aviso automático entre sistemas, esos que se explican en el artículo sobre webhooks: recibir lo mismo dos veces es normal, y el sistema que lo recibe no puede adivinar por contexto si es un duplicado técnico o un segundo evento real. Necesita algo más que los propios datos del pago para decidirlo.

#La idea, antes de tener nombre

La solución no consiste en que el sistema «adivine mejor». Consiste en cambiar la pregunta: en vez de intentar distinguir a posteriori si dos peticiones son la misma repetida o dos distintas, se hace que cada intento lleve, desde el principio, una forma de identificarse como único. Así, si la misma orden de cobro llega dos veces, el sistema no tiene que adivinar nada: simplemente reconoce que ya la atendió, y la segunda vez no hace nada nuevo. Cobra una vez, aunque la orden haya llegado —o se haya reintentado— dos.

Esa propiedad —que repetir la misma operación dé exactamente el mismo resultado que hacerla una sola vez, sin efectos añadidos por repetirla— es lo que se llama idempotencia. La palabra en sí importa poco; lo que importa es la idea: una operación bien diseñada no debería poder ejecutarse «por accidente» una segunda vez solo porque alguien, con toda la razón del mundo, volvió a pulsar el botón.

#Cómo se resuelve, sin entrar en código

La forma habitual de conseguirlo es sencilla de entender aunque se implemente con código: cada operación que se pueda repetir —pagar, crear un pedido, enviar un aviso— se acompaña de un identificador único, generado en el momento en que el usuario pulsa el botón, no cada vez que se reenvía la petición. El sistema que recibe esa operación guarda una lista de los identificadores que ya ha atendido. Si llega uno nuevo, lo procesa. Si llega uno que ya conocía, no vuelve a ejecutar nada: simplemente responde «esto ya está hecho», con el mismo resultado que dio la primera vez.

Con ese único mecanismo, da igual que la petición se repita por una red lenta, por un usuario impaciente o por un fallo momentáneo del propio sistema: el resultado final es siempre el mismo, uno solo, por mucho que el intento se haya repetido varias veces por el camino.

Merece la pena fijarse en dónde se genera ese identificador, porque ahí se cae el mecanismo cuando está mal montado. Tiene que nacer una sola vez, cuando la persona decide pagar, y acompañar a todos los reenvíos posteriores de esa misma intención. Si se genera cada vez que la petición sale hacia el sistema de cobro, cada reintento llegará con un identificador distinto, el sistema los tomará por operaciones diferentes, y se habrá construido toda la maquinaria para acabar cobrando dos veces igual.

Nótese que la solución no es «hacer que la red vaya más rápido» ni «pedirle al cliente que no pulse dos veces». Ninguna de las dos cosas está en tu mano de forma fiable: la red a veces va lenta por motivos ajenos a cualquiera, y una persona nerviosa por si su pago ha ido bien va a volver a pulsar, haga lo que haga el diseño de la pantalla para pedirle paciencia. El identificador único no evita que la gente reintente. Simplemente hace que reintentar sea inofensivo.

#No es solo cosa de pagos

Aunque el ejemplo del datáfono es el más fácil de visualizar porque se traduce directamente en dinero, el mismo problema aparece en cualquier operación que se pueda disparar dos veces sin querer: enviar un correo de confirmación que llega duplicado porque el sistema de avisos se reintentó, crear un pedido dos veces porque el usuario pulsó «finalizar compra» dos veces seguidas, dar de alta al mismo cliente por duplicado porque el formulario se envió dos veces antes de que apareciera el mensaje de éxito. En todos los casos, el fondo del problema es idéntico al del pago, y la solución también: un identificador único por intento, y un sistema que recuerde cuáles ya atendió.

#La pregunta exacta que hacer a tu proveedor de pagos y a quien integre tu sistema

Antes de dar por buena cualquier integración que mueva dinero o cree registros importantes, plantea una pregunta muy concreta —no necesitas programar para hacerla—: «si por cualquier motivo la misma petición llega dos veces, ¿qué pasa?». Si la respuesta es clara —«se detecta y no se repite el efecto»—, hay un mecanismo de idempotencia detrás, aunque nadie use esa palabra al explicarlo. Si la respuesta es vaga, o nadie sabe contestarla con seguridad, esa es la señal de que el problema del datáfono que se queda pensando todavía no se ha resuelto en tu sistema, y solo es cuestión de tiempo que aparezca.