E-commerce: cómo evitar sobreventas con inventario y pedidos concurrentes
Dos compradores ven que queda una unidad disponible y completan el checkout casi al mismo tiempo. Si el sistema consulta el stock, procesa cada pedido y actualiza el inventario en operaciones separadas, ambos pueden recibir una confirmación aunque solo exista un producto físico.
Este problema no se resuelve únicamente mostrando un número actualizado en la tienda. Es necesario definir cómo se reserva el stock, cuándo se considera confirmado el pago, qué ocurre si el pago falla y cómo se libera una reserva que expiró o fue cancelada. En este artículo se presenta un flujo genérico para analizar estas decisiones sin depender de una plataforma de e-commerce o una pasarela de pago específica.
La disponibilidad mostrada no equivale a stock reservado
El primer error frecuente es tratar la cantidad visible en el catálogo como si fuera una garantía de compra. La disponibilidad que observa el cliente es una lectura informativa que puede quedar desactualizada inmediatamente después de cargar la página.
Conviene distinguir al menos estos conceptos:
- Stock físico o disponible para venta: unidades que el negocio considera aptas para ofrecer.
- Stock reservado: unidades apartadas temporalmente para un pedido que todavía no terminó su ciclo.
- Stock comprometido: unidades asociadas a pedidos confirmados que deben prepararse o despacharse.
- Stock liberado: unidades que vuelven a estar disponibles porque una reserva expiró, el pedido fue cancelado o el pago no fue confirmado.
Una forma simple de calcular la cantidad que se muestra al cliente es considerar el stock disponible menos las reservas activas. Sin embargo, esa cifra no debe interpretarse como una promesa de compra. La decisión definitiva debe ocurrir en el backend, en una operación que controle la concurrencia.
Por qué la consulta previa no es suficiente
Un flujo vulnerable suele verse así:
- El cliente consulta cuántas unidades quedan.
- El backend responde que hay una unidad.
- El comprador envía el pedido.
- El backend vuelve a consultar el stock.
- El backend descuenta la unidad.
Si dos solicitudes ejecutan los pasos de forma intercalada, ambas pueden leer el mismo valor antes de que alguna lo actualice. El problema no está en que la base de datos sea lenta o en que el frontend muestre información antigua: está en que la lectura y la modificación no forman una operación indivisible.
El flujo correcto: reservar, cobrar, confirmar y liberar
Un pedido concurrente necesita estados explícitos. Los nombres pueden variar según la implementación, pero separar las etapas ayuda a evitar ambigüedades entre el inventario y el pago.
- Pendiente: el pedido fue recibido, pero todavía no tiene stock reservado o la reserva aún no se completó.
- Reservado: una cantidad concreta quedó apartada durante un período limitado.
- Pago en proceso: el sistema está esperando la respuesta de la operación de pago.
- Confirmado: el pago fue aceptado y el pedido puede avanzar a preparación o despacho.
- Cancelado: el pedido no continuará y cualquier reserva asociada debe liberarse.
- Expirado: la reserva superó su tiempo permitido sin completar el flujo.
Un flujo genérico puede implementarse de la siguiente manera:
- El cliente envía el pedido con sus productos y cantidades.
- El backend valida precios, reglas comerciales y disponibilidad vigente.
- El sistema intenta crear una reserva de inventario de forma atómica.
- Si la reserva no es posible, el pedido se rechaza o queda pendiente según la experiencia definida.
- Si la reserva se crea, el sistema inicia el proceso de pago.
- Cuando el pago queda confirmado, el pedido cambia a confirmado y la reserva se transforma en stock comprometido.
- Si el pago falla, se cancela el pedido y se libera la reserva.
- Un proceso de expiración libera reservas que no recibieron una confirmación dentro del plazo establecido.
La reserva temporal es especialmente útil cuando el pago puede tardar, requiere interacción del usuario o depende de una respuesta externa. Aun así, reservar stock no significa que el pago esté aprobado. Son responsabilidades distintas y deben mantenerse separadas.
Actualización atómica: la defensa básica contra la sobreventa
Una actualización atómica intenta modificar el inventario solo si todavía existe cantidad suficiente. La condición y el descuento se ejecutan como una única operación lógica desde la perspectiva de la concurrencia.
De forma conceptual, la operación es equivalente a:
UPDATE inventory
SET available_quantity = available_quantity - :quantity
WHERE product_id = :product_id
AND available_quantity >= :quantity;
Después de ejecutar la operación, el sistema debe verificar cuántas filas fueron modificadas. Si no se modificó ninguna, no había stock suficiente o el producto ya no podía reservarse. No se debe continuar hacia el pago suponiendo que la reserva fue exitosa.
Este enfoque tiene varias ventajas:
- Evita el patrón inseguro de leer y actualizar por separado.
- Es sencillo de razonar para una reserva inmediata.
- Permite rechazar una solicitud cuando otra ya consumió la última unidad.
También tiene limitaciones. Si el sistema necesita saber quién posee la reserva, cuándo expira o qué pedido la originó, probablemente deberá registrar una entidad de reserva además del contador de inventario. Además, en una arquitectura distribuida, la operación atómica debe ejecutarse en el componente que tiene la autoridad sobre ese inventario; no conviene replicar la lógica de descuento en varios servicios sin un criterio claro de consistencia.
Control de concurrencia: pesimista, optimista y basado en reservas
No existe una única estrategia válida para todos los comercios. La elección depende del volumen de solicitudes, la duración del checkout, la criticidad del inventario y la capacidad de la arquitectura para manejar reintentos y fallos.
Bloqueo pesimista
Con un bloqueo pesimista, una transacción bloquea el registro del producto mientras valida y actualiza su inventario. Otra transacción que intente modificar el mismo registro debe esperar o fallar según la configuración de la base de datos.
Puede ser apropiado cuando la operación es breve y el inventario se mantiene en una base de datos transaccional. El riesgo aparece cuando el bloqueo se mantiene durante llamadas lentas, como una interacción con una pasarela de pago. En ese caso, una dependencia externa puede prolongar la transacción, aumentar la contención y dificultar la recuperación ante errores.
Control optimista
El control optimista utiliza una versión o condición que debe coincidir al momento de actualizar. Si otro proceso modificó el registro entre la lectura y la escritura, la operación falla y el backend debe volver a consultar, reintentar o informar que el stock cambió.
Esta alternativa puede funcionar bien cuando los conflictos son poco frecuentes y las operaciones son cortas. Cuando muchas solicitudes compiten constantemente por las últimas unidades, los reintentos pueden aumentar y hacer más compleja la experiencia de usuario.
Reserva temporal
La reserva temporal registra que un pedido posee cierta cantidad durante un período limitado. El stock disponible para nuevos compradores se calcula descontando las reservas activas, y un proceso posterior libera las que expiraron.
La reserva temporal representa mejor los checkouts que no se completan de inmediato, pero exige resolver problemas adicionales:
- Definir una duración y una política para las reservas vencidas.
- Evitar que la liberación ocurra después de una confirmación válida.
- Hacer que la creación, cancelación y expiración sean idempotentes.
- Considerar qué sucede si el proceso de expiración se ejecuta dos veces.
- Reconciliar reservas con pedidos y movimientos de inventario.
En muchos sistemas se combinan estas técnicas: una actualización atómica o un control de concurrencia protege la creación de la reserva, mientras que la reserva temporal modela el ciclo de vida del checkout.
Qué ocurre cuando el pago depende de un sistema externo
El pago introduce una frontera de consistencia. La base de datos de la tienda puede confirmar una reserva, pero no controla completamente cuándo responderá una pasarela, si la respuesta se perderá o si llegará una notificación duplicada.
Por eso es conveniente tratar el resultado del pago como un evento que puede llegar más de una vez y en un orden diferente al esperado. El backend debe identificar cada operación de manera única y procesar los cambios de estado de forma idempotente.
Por ejemplo, si una notificación de pago confirmado llega dos veces, la segunda no debería volver a descontar inventario ni crear un nuevo pedido. Del mismo modo, si el usuario reintenta enviar el checkout por un timeout, el sistema debería reconocer la solicitud original en lugar de crear una reserva duplicada.
Una estrategia habitual es asociar:
- Un identificador único para el pedido.
- Un identificador único para la reserva.
- Una clave de idempotencia para la creación del pedido o la operación de pago.
- Un registro de los eventos o transiciones ya procesados.
La confirmación del pago no debería depender únicamente de lo que el navegador informa al backend. La tienda debe validar la respuesta mediante el mecanismo confiable que proporcione la integración correspondiente y actualizar el pedido solo una vez verificado el estado permitido.
Errores frecuentes al diseñar inventario concurrente
Descontar stock al agregar el producto al carrito
Un carrito no siempre representa una intención firme de compra. Si cada carrito descuenta inventario indefinidamente, el stock puede quedar retenido por sesiones abandonadas. Si el negocio necesita proteger unidades durante el checkout, debe utilizar reservas con expiración y reglas explícitas, no un descuento permanente al agregar el producto.
Confiar en el frontend
El frontend puede mostrar mensajes de disponibilidad y hacer validaciones para mejorar la experiencia, pero no debe ser la autoridad para decidir si existe stock. La validación definitiva debe ejecutarse en el backend y en el componente que controla el inventario.
Usar una caché como fuente única de verdad
Una caché puede ayudar a reducir lecturas y mejorar la respuesta del catálogo, pero puede contener datos desactualizados. No debería reemplazar la operación transaccional que decide si una unidad puede reservarse, salvo que se haya diseñado explícitamente un mecanismo de consistencia y recuperación adecuado para ese caso.
Olvidar cancelaciones, expiraciones y reintentos
Un diseño que solo contempla el camino exitoso acumula reservas huérfanas o produce diferencias entre pedidos e inventario. Cada transición debe definir qué ocurre si el pago falla, el usuario cancela, una notificación se duplica, un proceso se reinicia o una solicitud queda sin respuesta.
Distribuir el inventario sin definir su autoridad
Cuando existen varios servicios o almacenes, es necesario aclarar dónde se decide la disponibilidad. Si dos servicios pueden descontar el mismo stock sin coordinación, el problema de concurrencia reaparece aunque cada servicio funcione correctamente de forma aislada.
Cómo validar que la integración realmente evita sobreventas
La prueba no debería limitarse a crear pedidos de uno en uno. Es necesario simular solicitudes concurrentes para el mismo producto y verificar tanto el resultado visible como el estado interno del sistema.
Al menos conviene comprobar los siguientes escenarios:
- Dos compradores intentan reservar la última unidad al mismo tiempo.
- Un pedido se reintenta después de un timeout.
- La confirmación de pago llega duplicada.
- El pago falla después de crear la reserva.
- La reserva expira mientras se procesa una respuesta externa.
- El proceso que libera reservas se ejecuta más de una vez.
- Un producto tiene inventario dividido entre varios almacenes o ubicaciones.
- El servicio de inventario se reinicia después de reservar y antes de confirmar el pedido.
Además de las pruebas funcionales, la observabilidad debe permitir responder preguntas concretas: qué pedido creó una reserva, cuándo se liberó, qué cantidad se descontó, qué evento confirmó el pago y por qué una solicitud fue rechazada.
Los registros deberían incluir identificadores correlacionables de pedido, reserva y operación. Las métricas pueden ayudar a detectar reservas expiradas, conflictos de concurrencia, pagos confirmados sin inventario comprometido y diferencias entre el inventario calculado y el inventario físico. Para detectar inconsistencias persistentes, una tarea de conciliación puede comparar pedidos, reservas y movimientos de stock.
Conclusión: la decisión central es definir quién puede reservar y durante cuánto tiempo
Evitar sobreventas requiere separar claramente la disponibilidad informativa, la reserva de stock, la confirmación del pago y la liberación por cancelación o expiración. La operación crítica debe ser atómica o estar protegida por un mecanismo de control de concurrencia; una consulta previa al descuento no es suficiente.
Para una tienda con checkout breve puede bastar una actualización condicional y transaccional. Cuando el pago tarda o depende de sistemas externos, una reserva temporal con expiración, idempotencia y procesos de conciliación ofrece un modelo más completo, aunque también introduce más estados y escenarios de recuperación.
Revisa qué sucede en tu tienda cuando dos pedidos intentan reservar el mismo stock. Si no puedes explicar qué ocurre con la reserva, el pago, la cancelación y la expiración, consulta por una evaluación de la integración para identificar riesgos antes de que se conviertan en sobreventas.
