Sistema de venta de boletos numerados (rifa). Dos usuarios pueden intentar reservar el mismo número al mismo tiempo, y si uno no completa el pago, el número debe liberarse solo después de un tiempo. Nada de cron jobs limpiando manualmente.
El problema
Reservar un boleto no es solo "marcar como ocupado". Necesitas:
- Que dos requests concurrentes no reserven el mismo número (overselling).
- Que una reserva no confirmada expire sola sin batch job ni scheduler.
- Que la limpieza no dependa de que el usuario cierre bien el navegador o de que tu servidor no se caiga.
Intentar resolver esto con SELECT ... FOR UPDATE o lógica de aplicación con timestamps y un cron revisando "reservas viejas" funciona, pero es frágil: agregás un punto de falla más (el cron) y una ventana de carrera entre el check y el insert.
La solución: TTL index + índice único compuesto
MongoDB expira documentos automáticamente con un TTL index, sin que tu aplicación tenga que hacer nada:
// colección: reservas
db.reservas.createIndex(
{ creadaEn: 1 },
{ expireAfterSeconds: 900 } // 15 minutos
)
Mongo corre un background thread cada 60s que borra documentos donde creadaEn ya pasó el umbral. No hay cron, no hay job externo, no hay dependencia de tu capa de aplicación estando viva.
Para el overselling, el índice único compuesto es la pieza que realmente previene la doble reserva:
db.reservas.createIndex(
{ rifaId: 1, numeroBoleto: 1 },
{ unique: true }
)
El flujo completo
Document reserva = new Document()
.append("rifaId", rifaId)
.append("numeroBoleto", numero)
.append("usuarioId", usuarioId)
.append("estado", "PENDIENTE")
.append("creadaEn", new Date());
try {
collection.insertOne(reserva);
// reserva exitosa, boleto bloqueado por 15 min
} catch (MongoWriteException e) {
if (e.getError().getCategory() == ErrorCategory.DUPLICATE_KEY) {
// alguien ya tiene ese número reservado (o comprado)
throw new BoletoNoDisponibleException(numero);
}
throw e;
}
Si la reserva se confirma (pago aprobado), cambio estado a CONFIRMADA y remuevo el campo creadaEn (o lo muevo a otra colección de ventas confirmadas) para que el TTL deje de aplicar — un documento sin el campo indexado nunca expira.
collection.updateOne(
Filters.eq("_id", reservaId),
Updates.combine(
Updates.set("estado", "CONFIRMADA"),
Updates.unset("creadaEn")
)
);
Por qué esto y no un lock distribuido
Un lock con Redis o similar añade una pieza de infraestructura más para mantener viva. Acá el índice único ya es la garantía de exclusión mutua (Mongo la resuelve internamente), y el TTL resuelve la expiración sin estado adicional. Menos piezas moviéndose, menos que se puede romper.
Resumen para copiar y pegar
- TTL index sobre el campo de fecha de creación → expiración automática sin cron.
- Índice único compuesto sobre
(entidadId, recurso)→ previene doble reserva a nivel de base de datos, no de aplicación. - Al confirmar, quita el campo indexado por TTL para que el documento se vuelva permanente.
Top comments (0)