Quan vam publicar l'entrada sobre Lueira el març, la vaig descriure a alt nivell: una plataforma de reserves i gestió pensada per a escoles d'esports de muntanya — esquí, caiac, barranquisme, escalada, totes aquelles activitats on un "producte" és en realitat una persona, una peça de material i un forat al calendari passant al mateix temps. A la web ho resumim així: "Motor de reserves" — una botiga en línia que ven activitats, cursos i lloguers, connectada automàticament al calendari, a l'estoc i als monitors. Aquesta frase amaga la major part de l'enginyeria complicada. Vull repassar què fa que això sigui realment diferent de reservar una habitació d'hotel, i a grans trets com ho vam abordar.
1. Una reserva no és un recurs, en són diversos
Un motor de reserves d'hotels només ha de vigilar un eix: si aquella habitació és lliure aquelles dates. Un motor de reserves per a una escola d'esquí o de caiac ha de satisfer diverses restriccions independents alhora, i totes s'han de complir per a la mateixa reserva exacta:
- Un monitor disponible en aquell horari, qualificat per a aquella activitat, i que parli l'idioma que necessita el client.
- Material de la talla i quantitat correctes — un vestit de neoprè d'una talla, un caiac, uns esquís — que no estigui ja assignat a algú altre durant aquella franja.
- Una franja horària que no se solapi amb res més a què ja estigui compromès aquell monitor, o aquell material.
- Aforament de grup: una classe pot admetre quatre alumnes, una classe privada n'admet un.
Cap d'aquests és "el" recurs que es reserva — la reserva és en realitat la intersecció de tots ells. Així que en lloc de modelar un calendari amb caselles que es marquen, vam acabar modelant una franja reservable com una funció: donada una activitat, una data i una durada, quines combinacions de monitor + material + aforament estan lliures simultàniament. Aquesta funció és la que crida de veritat la cerca de l'aparador, i també la que crida el calendari del backoffice quan algú de l'equip arrossega una reserva a mà.
2. La cursa per l'última plaça
Un cop acceptes que una "franja reservable" és un conjunt de recursos que es comproven junts, topes amb el problema clàssic de qualsevol sistema d'inventari: dues persones intentant comprar la mateixa última unitat al mateix temps. La versió ingènua d'un flux de reserva — comprovar disponibilitat, i després inserir la reserva unes peticions més tard — té una cursa evident: dos clients poden passar la comprovació de disponibilitat abans que s'escrigui cap de les dues reserves, i tots dos marxen pensant que han reservat l'últim caiac.
Ho resolem amb una transacció que bloqueja exactament les files de què depèn una reserva abans de decidir si pot continuar:
-- exemple simplificat, no és codi real de producció
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 mecanisme exacte importa menys que el principi: la comprovació i la reserva d'un recurs han de ser una sola operació atòmica, no dos passos separats entre els quals una petició es pugui interrompre.
3. Mantenir la botiga honesta en temps real
La pàgina pública de reserves i el backoffice que fa servir el personal d'una escola no poden mostrar dues realitats diferents. Si un monitor bloqueja demà al matí al backoffice, aquella franja ha de desaparèixer de la botiga en línia a l'instant — no després que es refresqui una memòria cau. El catàleg — descripcions, preus, fotos — es pot posar en memòria cau de manera agressiva, perquè gairebé no canvia mai. La disponibilitat no pot.
L'únic lloc on sí que deixem que la vista del client vagi per darrere de la realitat a propòsit és el checkout. En lloc de tornar a executar tota la cerca cada vegada que emplena un camp, posem una retenció breu sobre els recursos concrets en el moment que comença el checkout: la franja, el material, el preu d'aquell moment, reservats per a ell durant uns minuts.
4. Què encara voldríem canviar
Les retencions són una eina tosca — un client que es distreu deu minuts perd la seva plaça encara que ningú no l'hi hagi "robat" amb mala intenció, i ajustar la finestra de retenció és un compromís real entre conversió i equitat. Encara no tenim llista d'espera per a les classes plenes. I les reserves de grup, on part del grup té una restricció i la resta en té una altra, encara necessiten un tractament més fi.
Aquesta és, més o menys, la forma del problema que "Motor de reserves" ha de resoldre cada vegada que algú reserva un curs, un lloguer o una activitat a través de Lueira. És menys qüestió de codi enginyós i més qüestió de ser honestos sobre de què depèn realment una sola reserva.
