El principio de idempotencia

Uno de los principales problemas en arquitecturas distribuidas es garantizar que múltiples ejecuciones con los mismos parámetros sobre el mismo recurso afectará al sistema como si se hubiera ejecutado 1 o N veces.

En realidad la idempotencia es un término matemático que dice “la idempotencia es la propiedad para realizar una acción determinada varias veces y aun así conseguir el mismo resultado que se obtendría si se realizase una sola vez”. Sin embargo, no quiero aburrirte con matemáticas, así que lo trataremos de explicar desde el punto de vista del software.

La importancia de la idempotencia

A pesar de que el termino idempotencia no es muy conocido, los problemas que pueden producir son mucho más frecuente de los que podríamos pensar, así que para comprender mejor su importancia analicemos el siguiente caso.

Tenemos una aplicación móvil que levante pedidos de los clientes, por tal motivo, la aplicación la tiene instalada varios vendedores que están en la calle ofreciendo los productos. Cuando un cliente quiere comprar alguno de nuestros productos, el vendedor levante el pedido desde la aplicación y posteriormente lo envía a nuestro ERP para procesar el pedido y programar la entrega del mismo.

Hasta aquí todo bien, pues es la clásica aplicación de creación de pedidos, sin embargo, un problema muy frecuente con las aplicaciones móviles es la conectividad con Internet, ya que hay zonas en las que no hay buena recepción o simplemente está no se tiene acceso al servicio, en tal caso, la aplicación lo que hace es que guarda el pedido en el celular y posteriormente lo sincroniza con el ERP.

Cuando la aplicación envíe el request al ERP, enviará un mensaje como el siguiente:

URL: https://myapp.com/orders
Method: POST
Headers:
   Accept: application/json
   Content-Type: application/json
Body:
{
	"customer": "oscar",
	"regDate": "2021-01-29T12:00:00Z"
	"productos": [
		{
			productId: 1,
			quantity: 10
		}
	]
}

Y obtendrá una respuesta como la siguiente:

URL: https://myapp.com/orders
Status: 201 CREATED
Headers:
   Content-Type: application/json  
Body:
{
	"id": 1,
	"customer": "oscar",
	"regDate": "2021-01-29T12:00:00Z"
	"productos": [
		{
			productId: 1,
			quantity: 10
		}
	]
}

Esto que estamos viendo es el escenario ideal, donde cada solicitud al servidor tiene una respuesta, sin embargo, es posible que durante el proceso de sincronización el pedido sea recibido por el ERP, pero por problemas de conectividad, la aplicación móvil no reciba la respuesta del ERP, lo que dará como resultado que el ERP creará el pedido, pero la aplicación móvil no tendrá una respuesta, por lo que el usuario asumirá que hay un problema con el envío, así que reintentará enviar nuevamente el pedido, lo que provocará que una solicitud idéntica a la anterior sea enviada al servidor, y una nueva orden sea creada en el ERP. Si este error continúa, el usuario seguirá enviando el pedido al ERP hasta tener éxito, lo que provocaría que tuviéramos N pedidos en el ERP.

Otro caso similar se da cuando por errores en el diseño de la aplicación,  el usuario puede presionar repetidas veces el botón de guardar sin que este se bloqué hasta recibir una respuesta. Este caso provoca que el usuario pueda enviar N veces la misma solicitud al servidor, creando también N pedido en el ERP.

Otro caso en el que se puede presentar este problema es cuando enviamos las solicitudes de creación por colas de mensajes (Queue), ya que no hay garantía de que un mensaje que enviemos sea entregado al destinatario, así que es posible que reenviemos el mensaje en más de una ocasión.

Cómo sortear la idempotencia

Bueno, en este punto creo que ya nos queda claro el problema con aplicaciones que no tiene presente este problema, así que ahora nos enfocaremos en como evitar este problema para cumplir con el principio de la idempotencia.

Aunque existen varias formas de evitar este tipo de problemas, sobresalen dos técnicas que son las más utilizadas, las cuales explicamos a continuación:

Identificador Virtual

Esta técnica se utiliza sobre en todos en entornos síncronos, donde el cliente se queda esperando hasta obtener la respuesta, y consiste en dos pasos.

El consumidor deberá solicitar al servidor la creación de un identificador virtual del objeto que va a crear, en este caso, podría tener un servicio que regresa un identificador para un pedido, de esta forma, el cliente primero solicita la creación del identificador mediante un payload vacío:

URL: https://myapp.com/orders
Method: POST
Headers:
   Accept: application/json
   Content-Type: application/json
Body:

Como resultado, el servidor le regresará el identificador generado del lado del servidor, tal y como se muestra a continuación:

URL: https://myapp.com/orders
Status: 201 CREATED
Headers:
   Content-Type: application/json  
Body:
{
	"id": 1
}

Ya con el identificador creado del lado del servidor, se puede procede a actualizar el pedido con el identificador retornado por el servidor mediante una segunda llamada al API:

URL: https://myapp.com/orders/1
Method: PUT
Headers:
   Accept: application/json
   Content-Type: application/json
Body:
{
	"customer": "oscar",
	"regDate": "2021-01-29T12:00:00Z"
	"productos": [
		{
			productId: 1,
			quantity: 10
		}
	]
} 

Podrás observar dos cambios significativos en este segundo request, y es que el ID del pedido ya no viaja a nivel del Payload, si no que lo enviamos como parte de la URL, ya que estamos realizando una actualización concreta sobre un pedido del cual ya conocemos el identificador.

El segundo cambio importante es que realizamos la llamada por el método PUT en lugar del método POST, ya que estamos actualizando el pedido.

Desde luego que esta estrategia plantea nuevos problemas, por ejemplo, que pasa si ejecuto el servicio para crear el identificador y no obtengo una respuesta. Este escenario sería parecido al problema original, ya que el servidor si crearía el identificador virtual, sin embargo, este solo sería un identificador y no un pedido, por lo que podríamos crear cuantos identificadores virtuales sin precisamente crear pedidos.

La segunda duda es, que pasaría si envío la solicitud de actualización (PUT) en más de una ocasión, pues bien, la respuesta es simple, por que solamente la estamos actualizando, por lo que si se envía en más de una ocasión solo actualizaremos el registro existente, no creando nuevos registros.

Identificador Único Universal (UUID)

Esta estrategia consiste en que cada objeto que es creado, debe ser asociado a un Identificado Único Universal (UUID), el cual deberá ser asignado en el momento en que un objeto es creado por primera vez, de esta forma, el sistema que crear por primera vez un objeto, será el responsable de asignar este identificador.

Un dato importante a resaltar es que el UUID puede ser independiente del Identificador de base de datos (llave primaria), ya que más bien, el UUID será un identificado de integración,  de tal forma que podrá ser replicado por todos los sistemas por lo que viaje, de esta forma, es posible rastrear un objeto en todos los sistemas por lo que pase.

En este orden de ideas, podemos ver que el flujo de creación de ordenes se ve de la siguiente forma. Cuando el usuario levante un pedido en la aplicación móvil, la misma aplicación le genera un UUD, luego, cuando quiere enviar el pedido al ERP, este lanza un request como el siguiente:

URL: https://myapp.com/orders
Method: POST
Headers:
   Accept: application/json
   Content-Type: application/json
Body:
{
	"UUID": "2da46052-584f-11eb-ae93-0242ac130002",
	"customer": "oscar",
	"regDate": "2021-01-29T12:00:00Z"
	"productos": [
		{
			productId: 1,
			quantity: 10
		}
	]
}

Si prestamos atención en el request anterior, podemos observar dos cosas, la primera es que el request es sobre el método POST, lo que quiere decir que efectivamente estamos solicitando la creación de un nuevo pedido. El segundo punto es que estamos enviando el parámetro UUID como parte del objeto del pedido. Mas adelante analizaremos que implica todo esto, por ahora veamos el resultado de esta ejecución:

URL: https://myapp.com/orders
Status: 201 CREATED
Headers:
   Content-Type: application/json  
Body:
{
	"id": 1,
	"UUID": "2da46052-584f-11eb-ae93-0242ac130002",
	"customer": "oscar",
	"regDate": "2021-01-29T12:00:00Z"
	"productos": [
		{
			productId: 1,
			quantity: 10
		}
	]
}

En la respuesta podemos ver que el servidor nos arrojado nuevamente el UUID que le enviamos para la creación, pero además, nos retorna el ID, lo cual puede resultar confuso, pues ahora tenemos dos Identificadores, sin embargo, cada uno juega una papel diferente, pues el Id es el identificar único del pedido en el ERP, mientras que el UUID es el identificar universal, por lo que ahora es posible relaciona el pedido en la app móvil con el del ERP mediante este identificador, además, si este pedido tiene que ser replicado del ERP a otro sistema, puede seguir usando el mismo UUID para sincronizarlo con el este tercer sistema.

Pero bien, pasemos a los que nos interesa, que pasa ahora si enviamos la misma solicitud más de una vez. Pues bien, si volvemos a enviar este misma solicitud al ERP, este podrá detectar que ya existe un pedido asociado con ese UUID, por lo que pueden pasar dos cosas, la primera es que marque un error al crear el pedido, indicando que el pedido ya existe, o en su lugar realizar un update del pedido, de esta forma, si la primera solicitud creo el pedido, la segunda solicitud no creará un nuevo pedido, si no más bien, solo lo actualizará, por lo que en cliente recibirá el mismo Id con cada ejecución.

Conclusiones

Como hemos podido analizar el principio de idempotencia es un problema que se puede presentar muchas más veces de lo que parecería, por lo que es importante garantizar que el sistema se comporte adecuadamente aun cuando el mismo request sea enviado muchas veces, sobre todo en sistemas distribuidos.

También analizamos dos formas se garantizar el principio de idempotencia, y cada estrategia se podrá adecuar mejor a cada situación, sin embargo, creo que la estrategia por UUID es la mejor, ya que es mucho más simple de implementar, pero sobre todo, permite una rastreabilidad entre todos los puntos de la integración, ya sea entre dos sistemas o varios sistemas y puntos intermedios, como colas de mensajes o persistencia en base de datos.

Finalmente, en este artículo nos centramos en las peticiones por HTTP, como es el caso de REST, pero cabe mencionar que este problema se da con cualquier tipo de tecnología o protocolo de comunicación, por lo que no es un problema ajeno solo a REST.

10 thoughts to “El principio de idempotencia”

  1. Una pregunta Oscar y espero no resulte muy boba.

    En algunos escenarios, es necesario limitar los campos que pueden ser actualizados en un recurso, por regla de negocio. Supongamos que el nombre del comprador no se puede editar una vez realizado el pedido, por lo que un nuevo pedido no sería igual que una edición de un pedido. ¿Cómo se puede manejar ese escenario ya que en el ejemplo estas usando PUT para ambas acciones?. Saludos y muy buen artículo.

    1. Usa el UUID, cuando crees el pedido, le asignas el UUID, y ese lo envías al otro sistema, así si envian más de una ves ese pedido, lo podrás detectar.

  2. Hola Oscar me pareció super útil el concepto me dejo más claro el tema y no mezclar con el código de la tablas por ejemplo. Gracias por compartir tus conocimientos.

  3. Sí se trabaja correctamente con un Identity manager, que sería una tercer solución el bus empresarial del servidor podría detectar que esa petición ya fue hecha por el usuario y actuar en respuesta. Problemas de conexión no deberían ser un impedimento para comprometer la integridad de un sistema.
    Me da curiosidad cómo trabaja este mecanismo un ERP empresarial del nivel de SAP por ejemplo.

    1. El problema con el Bus es que negará el paso del request, lo que puede implicar que el primer request nunca llego al destinarario o no lo proceso correctamente, por lo que el bus asumirá que si y no permitirá el envio nuevamente.

  4. Hola! Luego de leer el post completo me surgieron algunas dudas:
    Suponiendo que tengo dos microservicios en donde el 2do se conecta a la DB. Se le hace una petición de creación al primer microservicio el cual genera el UUID, se lo asigna al objeto creado y envía este objeto al 2do microservicio. Este último debe guardarlo en la DB junto con el UUID. En este ejemplo el 1er microservicio no recibe respuesta por parte del 2do microservicio (que sí realizó la inserción en la DB pero por problemas de red su respuesta no llegó al primer microservicio). En este caso, cuando se envíe de vuelta el objeto creado, el 2do microservicio debe buscar en la DB y comparar si existe ese UUID único que se asignó previamente? Entiendo que sí !

    Gracias!

    1. Lo que dices es correcto, lo que pasaría aquí es que cuando el 1° microsrvicio envíe por segunda vez el mismo UUID, el segundo servicio responderá que ya existe un registro con ese UUID, por lo que el primero ahora tendrá que consultar el registro creado por medio de ese UUID para responderlo.

  5. Hola Oscar. Lo primero enhorabuena por el artículo, muy interesante.

    Nosotros estamos teniendo un problema en la integración con un sistema a de terceros. Es una aplicación de gestión de citas. El problema es que si lanzamos un PUT de una cita donde mantenemos la fecha y hora de la cita, pero cambiamos por ejemplo un comentario del cliente, el sistema nos dice que el hueco de cita está ocupado (está detectando a la propia cita).

    ¿Es correcto este comportamiento en tu opinión?

    Gracias y un saludo.

    1. El PUT siempre debe ser sobre el ID del registro, si la fecha es el ID no debería de marcar error, ya que le estás indicando que actualiza exactamente ese registro, pero si lo hace sobre cualquier otro campo que no es el ID, no debería de permitirlo. En tu caso, si la fecha no es el ID y lo permite, estamos en un mal diseño del servicio

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *