Candado cerrado sobre una reja blanca
Guías

Por qué tu tienda nunca debe vender dos veces el mismo producto (y cómo lo evitamos)

La doble venta no es mala suerte: es un error de diseño que se paga con reembolsos, disculpas y confianza. Así lo resolvimos en URG Technology, donde casi todo es unidad única.

Damorus Projects8 min de lectura

Imagina el peor par de mensajes que puede recibir un cliente. Primero: "gracias por tu compra". Al día siguiente: "lo sentimos, ese equipo ya estaba vendido". En una tienda en línea eso no es mala suerte, es un error de diseño, y tiene nombre: doble venta.

Nos lo encontramos de frente construyendo , una tienda de computadores reacondicionados en Bogotá. Ahí casi todo es unidad única: cada equipo tiene su estado, su precio y su historia. Dos personas queriendo el mismo portátil al mismo tiempo dejan de ser un escenario raro y se vuelven un riesgo diario.

CONFIANZASIN REGLASCON RESERVA1 disponible2 pagos · 1 equipoPAGO 1PAGO 2AGOTADO-1 reembolso-1 disculpa-1 confianzaconfianza intacta ✓
Dos pagos por un solo equipo cuestan un reembolso, una disculpa y algo que no se repone: la confianza.

Qué cuesta de verdad vender dos veces

Una doble venta se paga tres veces. Primero el reembolso, con su trámite. Después la disculpa, que se lleva el tiempo de alguien. Y al final lo más caro: la confianza de una persona que ya había decidido comprarte y que ahora duda. Si tu cliente estaba comprando algo usado, sin tocarlo, esa duda vale doble.

Hay un primo silencioso del problema: el stock fantasma. Son los clientes que empiezan a pagar, abandonan y dejan el equipo apartado para siempre. Nadie reclama, nadie se queja, y la tienda deja de vender un producto que en realidad está disponible.

Por dónde se cuela

La primera versión de URG confiaba en lo que veía el navegador de cada cliente: la pantalla consultaba el stock, lo restaba y avisaba. Parece suficiente hasta que dos personas llegan juntas. Las dos ven "1 disponible", las dos pagan y la tienda deja pasar a las dos. La causa de fondo era que el inventario se descontaba en el lugar equivocado y sin turnos.

Había un segundo problema de diseño. Esa primera implementación necesitaba que un administrador tuviera el panel abierto para procesar las confirmaciones de pago. Si cerraba el computador o se iba a dormir, la tienda dejaba de actualizar el inventario. Una tienda que depende de que alguien esté despierto no es una tienda en línea: es un mostrador con horario.

La reserva atómica, explicada sin tecnicismos

Atómico, aquí, significa "todo o nada": una operación que no se puede partir por la mitad. Cuando alguien intenta apartar un equipo, el servidor bloquea ese registro por un instante, revisa cuántas unidades quedan realmente libres y solo entonces aparta. Si dos intentos llegan juntos, uno espera al otro: el segundo encuentra el stock en cero y recibe un "agotado" antes de pagar.

Encima va un cinturón de seguridad: una regla dentro de la base de datos que hace imposible que lo apartado supere lo que existe. Aunque alguien se equivoque programando mañana, la base rechaza el movimiento. Y cada reserva guarda por qué medio se hizo, cuándo se creó y cuándo vence.

STOCK: 1STOCK: 0CLIENTE 1CLIENTE 2¡comprar!¡comprar!apartado ✓agotado0102TURNOSERVIDOR0102uno espera al otro · nadie paga de más
El servidor reparte turnos. Dos intentos simultáneos no se atropellan: uno aparta y el otro se entera antes de pagar.

Así se ve la idea en el lenguaje de una base de datos. Es un ejemplo simplificado para entenderla, no el código de URG:

sql
BEGIN;

-- 1. Bloquea la fila mientras decide: el otro intento espera su turno.
SELECT stock, apartados
  FROM productos
 WHERE id = 'pc-123'
   FOR UPDATE;

-- 2. Aparta solo si de verdad queda una unidad libre.
UPDATE productos
   SET apartados = apartados + 1
 WHERE id = 'pc-123'
   AND stock - apartados >= 1;
-- Si no quedaba ninguna, no se actualiza ninguna fila: eso es un "agotado".

COMMIT;

-- 3. El cinturón de seguridad: nunca se aparta más de lo que existe.
ALTER TABLE productos
  ADD CONSTRAINT reservas_sin_exceso CHECK (apartados <= stock);
Ejemplo simplificado: bloquear la fila, apartar solo si queda unidad libre y dejar una regla que la base nunca permite romper.

Las dos piezas llevan años en cualquier base de datos seria. FOR UPDATE bloquea las filas que seleccionas, de modo que otra transacción que intente modificarlas queda esperando hasta que la primera termine. [1] Y una restricción CHECK obliga a que el valor de una columna cumpla siempre una condición. [2] No hay magia: hay orden.

Tres relojes que trabajan sin nadie mirando

Apartar bien es la mitad del trabajo. La otra mitad es soltar lo que nadie usó:

  • Solo un pago confirmado toca el inventario. Abrir la pasarela no aparta nada. Cuando el pago se aprueba, el aviso llega al servidor, que valida su origen, consulta el estado real, descuenta el stock y dispara los correos.
  • Un barrido cada dos minutos. Un proceso programado revisa las reservas vencidas: libera los pagos por transferencia que no subieron comprobante a tiempo, cierra los intentos de pasarela abiertos por más de una hora y marca esos pedidos como expirados.
  • Cancelación cruzada. Cuando un equipo se vende por una vía, el sistema recorre las reservas pendientes del mismo producto por la otra y las cancela, en ambas direcciones.
STOCK FANTASMAapartados que nadie pagólibres: 2libres: 4LIBRELIBREapartadoLIBRE8 minvencidoapartadoLIBRE1 hvencido
Apartar el puesto con la chaqueta funciona un rato. Después alguien pasa, la retira y la silla vuelve a estar libre.

Quien pierde la carrera no tiene por qué perder la compra

Aquí está la parte que casi nadie diseña. El cliente que llegó segundo no ve un error técnico: ve un mensaje en rojo, "¡Alguien fue más veloz!", con una explicación corta y un botón que abre el catálogo filtrado por la categoría del equipo que se perdió. Además recibe un correo de disculpa con un cupón del 5 %.

La primera versión de ese aviso decía "¡Rayos! Te ganaron por un milisegundo". La cambiamos porque el tono importa: esa persona no perdió un juego, perdió un computador que quería. El aprendizaje es doble. La integridad se resuelve en el servidor, y hasta una mala noticia puede terminar en otra venta.

Lo peligroso no es que falle: es que falle en silencio

Una reserva huérfana no avisa. Un aviso de pago que nunca llegó, tampoco. Por eso en URG construimos Centinela, un módulo de salud que se dedica a desconfiar: busca reservas vencidas que no se liberaron, pagos sin confirmar por más de una hora, productos con más unidades apartadas que existentes y funciones del servidor con errores recientes. Cada alerta llega con su gravedad y su botón para resolverla.

“Un sistema que no puede decir cuándo está mal va a estar mal sin que nadie lo sepa.”

Aprendizaje del proyecto URG Technology

Cinco preguntas para saber si tu tienda está expuesta

  1. 1¿Vendes piezas únicas o pocas unidades? Reacondicionados, repuestos, arte, lotes limitados: ahí el riesgo es de todos los días.
  2. 2¿Aceptas más de un medio de pago? Cada medio nuevo es otra puerta hacia el mismo producto.
  3. 3¿Quién descuenta el stock? Si la respuesta es "la página", tienes el problema que tuvimos nosotros.
  4. 4¿Qué pasa con un carrito que empieza a pagar y se va? Si nadie lo suelta, ese producto queda invisible para todos los demás.
  5. 5¿Tu tienda necesita que alguien tenga el panel abierto? Si la respuesta es sí, no estás vendiendo de noche.

Vender dos veces el mismo producto es de esos errores que solo se ven cuando ya pasaron. Por eso se diseñan antes. Si tienes una tienda con unidades únicas, o estás por construirla, cuéntanos qué vendes y te decimos cómo blindaríamos tu inventario.

Fuentes

Referencias citadas en el texto con la notación [n]. Cada enlace fue verificado y es de acceso público.

  1. 1

    Explicit Locking: bloqueos a nivel de fila con FOR UPDATE (Documentación oficial de PostgreSQL)

    Fuente primaria: FOR UPDATE bloquea las filas seleccionadas y otras transacciones que intenten modificarlas esperan hasta que la transacción actual termina.

    https://www.postgresql.org/docs/current/explicit-locking.html
  2. 2

    Constraints: restricciones CHECK (Documentación oficial de PostgreSQL)

    Fuente primaria: una restricción CHECK obliga a que el valor de una columna cumpla una expresión booleana en cada escritura.

    https://www.postgresql.org/docs/current/ddl-constraints.html

¿Le sirve a alguien? Compártelo.

Tu proyecto con nosotros puede empezar hoy.

Cuéntanos qué quieres lograr, o eso que siempre quisiste que tu proyecto lograra, y te decimos cómo lo haríamos y cuánto cuesta. Si lo puedes imaginar, lo podemos hacer realidad.

o escríbenos por WhatsApp (+49 176 4454 2881) o a Alejandro@damorus.com