El problema del intercambio bidireccional en PostgreSQL
Modelar un marketplace de intercambio de furgonetas en PostgreSQL. Un alquiler es A→B. Un intercambio es A⇄B con ventanas de fechas independientes. La diferencia rompe la mayoría de esquemas estándar de marketplace.
Cuando empecé a construir ExchangeYourVan, asumí que el modelo de datos sería cercano a un marketplace de alquiler estándar — listings de furgonetas, calendarios de disponibilidad, solicitudes de reserva. La mecánica de intercambio resultó romper casi todos los supuestos que tenía.
Un alquiler: la persona A entrega el vehículo a la persona B para las fechas X-Y. Una dirección. Un conjunto de fechas. Una comprobación de disponibilidad.
Un intercambio: la persona A entrega el vehículo a la persona B para las fechas X-Y, Y la persona B entrega el vehículo a la persona A para las fechas P-Q. Dos direcciones. Dos conjuntos de fechas independientes. Dos comprobaciones de disponibilidad simultáneas. Las fechas no tienen por qué solaparse.
Por qué los esquemas estándar de marketplace fallan
La mayoría de esquemas de marketplace modelan una booking como (item_id, requester_id, owner_id, start_date, end_date). Eso funciona para alquiler. Para intercambio, necesitas:
- Dos items (la furgoneta de A y la furgoneta de B)
- Dos ventanas de disponibilidad (potencialmente distintas)
- Un compromiso mutuo que cualquiera de las partes puede proponer, contraoferecer o aceptar
- Bloqueo independiente de disponibilidad en ambos vehículos durante la negociación
Si modelas esto como dos reservas separadas, pierdes la atomicidad: la reserva de A puede confirmarse mientras la de B está todavía pendiente, dejándote con un medio-intercambio que es imposible resolver limpiamente.
El esquema al que llegué
CREATE TABLE exchanges (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
van_a_id uuid REFERENCES vans(id),
van_b_id uuid REFERENCES vans(id),
dates_a daterange NOT NULL, -- cuando B usa la furgoneta de A
dates_b daterange NOT NULL, -- cuando A usa la furgoneta de B
status exchange_status NOT NULL DEFAULT 'proposed',
proposed_by uuid REFERENCES users(id),
created_at timestamptz DEFAULT now()
);
El tipo daterange es nativo de PostgreSQL y soporta consultas de solapamiento con el operador && — crítico para la comprobación de disponibilidad.
Bloqueo de disponibilidad
Antes de confirmar un intercambio, ambas ventanas de fechas necesitan comprobarse contra los intercambios confirmados existentes para cada furgoneta:
SELECT 1 FROM exchanges
WHERE (van_a_id = $1 AND dates_a && $2 AND status = 'confirmed')
OR (van_b_id = $1 AND dates_b && $2 AND status = 'confirmed')
Un índice GiST en las columnas de rango mantiene estas consultas rápidas a medida que crece el volumen de intercambios.
El compromiso mutuo es atómico: ambas partes se confirman en una única transacción, o ninguna. Sin medios-intercambios.
Lo que haría diferente
El enum status empezó con cuatro valores y creció hasta siete. La próxima vez diseñaría una máquina de estados completa desde el principio — con transiciones permitidas explícitas — en lugar de añadir estados de forma reactiva según aparecían los casos extremos.
El esquema central aguantó. Las transiciones de estado necesitaban más reflexión inicial.
¿Te suena este problema?
Si tienes algo parecido en tu operación y quieres hablarlo, escríbeme.
[email protected] →