Todos los artículos

Diseñar un motor de reservas para escuelas de deportes de montaña

Publicado 14 jun 20234 min de lectura
  • Producto
  • Ingeniería
  • Lueira
Diseñar un motor de reservas para escuelas de deportes de montaña

Ideas clave

  • Un motor de reservas para actividades físicas tiene que comprobar varios recursos restringidos a la vez: monitor, material y franja horaria.
  • Evitar dobles reservas exige que la comprobación de disponibilidad y la reserva sean una sola operación atómica, típicamente mediante una transacción de base de datos.
  • Las páginas públicas de reserva deben reflejar el estado real del backoffice en tiempo real, con una retención breve durante el checkout para mantener válidos el precio y la disponibilidad.

Cuando publicamos la entrada sobre Lueira en marzo, la describí a alto nivel: una plataforma de reservas y gestión pensada para escuelas de deportes de montaña — esquí, kayak, barranquismo, escalada, todas esas actividades donde un "producto" es en realidad una persona, una pieza de material y un hueco en el calendario ocurriendo al mismo tiempo. En la web lo resumimos así: "Motor de reservas" — una tienda online que vende actividades, cursos y alquileres, conectada automáticamente al calendario, al stock y a los monitores. Esa frase esconde la mayor parte de la ingeniería complicada. Quiero repasar qué hace que esto sea realmente distinto de reservar una habitación de hotel, y a grandes rasgos cómo lo abordamos.

1. Una reserva no es un recurso, son varios

Un motor de reservas de hoteles solo tiene que vigilar un eje: si esa habitación está libre esas fechas. Un motor de reservas para una escuela de esquí o de kayak tiene que satisfacer varias restricciones independientes a la vez, y todas tienen que cumplirse para la misma reserva exacta:

  • Un monitor disponible en ese horario, cualificado para esa actividad, y que hable el idioma que necesita el cliente.
  • Material en la talla y cantidad correctas — un traje de neopreno en una talla, un kayak, unos esquís — que no esté ya asignado a otra persona durante esa franja.
  • Una franja horaria que no se solape con nada más a lo que ya esté comprometido ese monitor, o ese material.
  • Aforo de grupo: una clase puede admitir cuatro alumnos, una clase privada admite uno.

Ninguno de estos es "el" recurso que se reserva — la reserva es en realidad la intersección de todos ellos. Así que en vez de modelar un calendario con huecos que se marcan, acabamos modelando una franja reservable como una función: dada una actividad, una fecha y una duración, qué combinaciones de monitor + material + aforo están libres simultáneamente. Esa función es la que llama de verdad la búsqueda del escaparate, y también la que llama el calendario del backoffice cuando alguien del equipo arrastra una reserva a mano.

2. La carrera por la última plaza

Una vez aceptas que una "franja reservable" es un conjunto de recursos que se comprueban juntos, te topas con el problema clásico de cualquier sistema de inventario: dos personas intentando comprar la misma última unidad al mismo tiempo. La versión ingenua de un flujo de reserva — comprobar disponibilidad, y luego insertar la reserva unas peticiones después — tiene una carrera evidente: dos clientes pueden pasar la comprobación de disponibilidad antes de que se escriba ninguna de las dos reservas, y los dos se van pensando que han reservado el último kayak.

Lo resolvemos con una transacción que bloquea exactamente las filas de las que depende una reserva antes de decidir si puede seguir adelante:

-- ejemplo simplificado, no es código real de producción
BEGIN;

SELECT id, available_qty
  FROM equipment_stock
 WHERE product_id = $1 AND size = $2
   FOR UPDATE;

SELECT id
  FROM instructor_slots
 WHERE instructor_id = $3
   AND slot_start = $4
   AND slot_end = $5
   AND booked = false
   FOR UPDATE;

UPDATE equipment_stock
   SET available_qty = available_qty - 1
 WHERE product_id = $1 AND size = $2 AND available_qty > 0;

UPDATE instructor_slots
   SET booked = true
 WHERE instructor_id = $3 AND slot_start = $4 AND slot_end = $5 AND booked = false;

INSERT INTO bookings (customer_id, product_id, instructor_id, slot_start, slot_end)
VALUES ($6, $1, $3, $4, $5);

COMMIT;

El mecanismo exacto importa menos que el principio: la comprobación y la reserva de un recurso tienen que ser una sola operación atómica, no dos pasos separados entre los que una petición se pueda interrumpir.

3. Mantener la tienda honesta en tiempo real

La página pública de reservas y el backoffice que usa el personal de una escuela no pueden mostrar dos realidades distintas. Si un monitor bloquea mañana por la mañana en el backoffice, esa franja tiene que desaparecer de la tienda online al instante — no después de que se refresque una caché. El catálogo — descripciones, precios, fotos — se puede cachear de forma agresiva, porque apenas cambia. La disponibilidad no puede.

El único sitio donde sí dejamos que la vista del cliente vaya por detrás de la realidad a propósito es el checkout. En vez de volver a ejecutar toda la búsqueda cada vez que rellena un campo, ponemos una retención breve sobre los recursos concretos en cuanto empieza el checkout: la franja, el material, el precio de ese momento, reservados para él durante unos minutos.

4. Qué seguimos queriendo cambiar

Las retenciones son una herramienta tosca — un cliente que se distrae diez minutos pierde su plaza aunque nadie se la haya "robado" con mala intención, y ajustar la ventana de retención es un compromiso real entre conversión y equidad. Todavía no tenemos lista de espera para las clases llenas. Y las reservas de grupo, donde parte del grupo tiene una restricción y el resto otra, siguen necesitando un tratamiento más fino.

Esa es, más o menos, la forma del problema que "Motor de reservas" tiene que resolver cada vez que alguien reserva un curso, un alquiler o una actividad a través de Lueira. Es menos cuestión de código ingenioso y más cuestión de ser honestos sobre de qué depende realmente una sola reserva.