Saltar al contenido
Volver a proyectos

Plataforma de venta de artículos numerados bajo alta concurrencia

Venta de artículos de stock finito y numerado, con apertura en horario anunciado. El mismo número no puede venderse dos veces, y el pico de apertura no puede tumbar la base de datos.

Case study anonimizado: nombres de cliente, datos de infraestructura y detalles sensibles se han omitido por confidencialidad.

Contexto

Plataforma con panel administrativo, API y tienda pública. Las ventas abren en fecha y hora anunciadas, lo que concentra la demanda en ventanas cortas, con mucha gente disputando los mismos números al mismo tiempo.

Desafío

Dos problemas que se agravan mutuamente. El primero es de corrección: el mismo número no puede venderse dos veces, nunca. El segundo es de carga: la apertura se anuncia, y la solución ingenua, reservar el número dentro del request, pone la base de datos en el camino crítico de todos al mismo instante.

Mi papel

Arquitectura de la plataforma, ingeniería backend, modelado de datos, pipelines asíncronos e infraestructura.

Responsabilidades

  • Arquitectura de la plataforma y del modelo de datos.
  • Ingeniería backend, con contextos delimitados por dominio.
  • Pipelines asíncronos de reserva, pago y expiración.
  • Integración con pasarelas de pago.
  • Infraestructura como código y pipeline de despliegue.
  • Autorización por rol y limitación de tasa.

Restricciones

  • Vender el mismo número dos veces es un problema de corrección, no de rendimiento. No hay margen de error.
  • El pico llega concentrado, en un horario que el propio negocio anunció.
  • Un producto puede tener millones de números, así que guardar cada número como una fila no escala.
  • Una reserva abandonada tiene que volver al stock sola, sin intervención manual.
  • Plataforma white-label: cada cliente con su dominio y su identidad visual.

Arquitectura

  • API en Elixir con Phoenix: la concurrencia masiva y el aislamiento de fallos son la forma nativa de la plataforma, que es exactamente la forma del problema.
  • Disponibilidad guardada como bitmap binario, un bit por número, en lugar de una fila por número. Un millón de números ocupa unos 125 KB.
  • Contadores de disponible, reservado y pagado mantenidos junto al bitmap, para responder cuántos quedan sin recorrer la tabla.
  • La reserva no ocurre dentro del request. La API graba el pedido, publica un job en la cola y responde de inmediato.
  • Los workers consumen la cola a un ritmo sostenible y hacen la reserva de verdad, dentro de una transacción.
  • Índice único sobre producto y número: es la base de datos la que arbitra quién se llevó el número disputado.
  • Un job programado devuelve al stock los números de reservas vencidas.
  • La tienda pública se genera como sitio estático por cliente, servido desde su propia distribución de CDN.

Principales decisiones técnicas

Bitmap binario en lugar de una fila por número.

Por qué

Un producto puede tener millones de números. Materializar cada uno como fila significa millones de registros que solo existen para decir que ese número está libre, y cada consulta de disponibilidad se convierte en un recorrido. Un bit por número responde lo mismo leyendo pocos KB, y los contadores al lado evitan contar la tabla entera en cada acceso.

Trade-offs

  • El número pierde identidad propia en la base: colgar metadatos por número exige una tabla aparte.
  • Actualizar el bitmap es leer, modificar y escribir, lo que vuelve obligatorio el lock.
  • Los contadores deben actualizarse bajo ese mismo lock, o empiezan a mentir.

Reservar fuera del request, con la cola absorbiendo el pico.

Por qué

Reservar dentro del request pone la base de datos en el camino crítico de todos en el mismo instante, que es justo cuando abren las ventas. Con la cola, la API solo tiene que grabar un pedido y publicar un mensaje, y la base pasa a ver un flujo constante en vez de una avalancha. La cola es el amortiguador, y puede crecer.

Trade-offs

  • La compra deja de ser instantánea: al usuario se le avisa de que sus números se están reservando, en lugar de recibirlos en el acto.
  • Con un pico grande la cola tarda minutos en drenar, y la interfaz tiene que asumirlo en vez de esconderlo.
  • Más piezas que operar y monitorizar: la cola, los workers y su backlog.
  • A cambio, el sistema se retrasa en vez de caerse.

El índice único de la base es la garantía. La comprobación previa es solo una optimización.

Por qué

La consulta de disponibilidad corre fuera de la transacción, así que es un filtro rápido y no una promesa: entre leer y escribir, otro pedido puede haberse llevado el número. Quien arbitra la disputa es el índice único sobre producto y número. Dos pedidos concurrentes por el mismo número chocan en el índice, el perdedor recibe una violación de unicidad y toda su transacción hace rollback. El lock pesimista no garantiza la unicidad: protege el estado derivado, el bitmap y los contadores, frente a una actualización perdida.

Trade-offs

  • El perdedor paga un rollback completo, en lugar de ser reasignado a un número sobrante.
  • La fila del bitmap se convierte en un punto caliente de escritura, serializando las reservas de ese producto.
  • Esto solo es sostenible porque la cola ya reguló la tasa de llegada: el lock nunca lo disputa el tráfico crudo de los usuarios, solo los workers.
  • Las dos decisiones forman un par. Ninguna funcionaría sin la otra.

Tienda estática por cliente, en lugar de renderizado multi-tenant en runtime.

Por qué

Cada cliente white-label recibe su propio build estático, con tema e identificador incrustados en tiempo de build, su certificado, su distribución de CDN y su prefijo en el bucket. Añadir un cliente pasa a ser un cambio de dato en la infraestructura, no un cambio de código.

Trade-offs

  • Cualquier cambio de tema exige rebuild, publicación e invalidación de CDN.
  • Se renuncia a la optimización de imagen del framework, que no funciona en export estático detrás de una CDN.
  • A cambio, el coste de origen por tienda queda cerca de cero y el aislamiento entre clientes ocurre en el borde.

Implementación

  • Panel administrativo, API y tienda pública, cada uno en su repositorio.
  • Contextos delimitados en el backend, separando pedidos, números, pagos y notificaciones.
  • Colas dedicadas por tipo de job, con concurrencia ajustada a la forma de cada carga.
  • Subida de archivos directa al object storage mediante URL prefirmada, sin pasar por la API.
  • Integración con pasarelas de pago detrás de una única interfaz.
  • Autenticación por JWT con roles distintos y, para el comprador, login sin contraseña mediante un código enviado a su teléfono.

Operación en producción

  • Infraestructura como código, con módulos versionados y estados separados por entorno.
  • Promoción a producción solo desde la rama de staging, impuesta por el propio pipeline.
  • Imagen Docker multi-stage, ejecutada con un usuario sin privilegios y con etiqueta inmutable por commit para permitir rollback.
  • Migraciones ejecutadas en su propio contenedor antes de que la aplicación arranque.
  • Cola de mensajes fallidos por pipeline, telemetría y logs estructurados por job.
  • Existe un job programado cuyo único propósito es reparar la brecha entre el commit en la base y la publicación en la cola: si el pedido se marca como pagado pero el encolado siguiente falla, sus números quedan atrapados como reservados. En vez de fingir que la escritura doble nunca falla, un reconciliador encuentra esos pedidos y los corrige.

Resultados

  • Vender el mismo número dos veces es imposible por construcción, arbitrado por la base de datos y no por una comprobación posterior.
  • La API sigue respondiendo durante la apertura, porque el pico va a la cola y no a la base de datos.
  • La disponibilidad de millones de números cabe en una estructura de pocos KB.
  • Las reservas abandonadas vuelven al stock sin que nadie tenga que actuar.
  • El fallo de escritura doble, que es conocido e inevitable, tiene reparación automática en lugar de un ticket de soporte.

Stack

  • Elixir
  • Phoenix
  • Ecto
  • PostgreSQL
  • Broadway
  • Amazon SQS
  • Next.js
  • TypeScript
  • Docker
  • Terraform
  • AWS