Planteamiento y contexto adecuado
Esta es una pregunta de diseño de bajo nivel y código. El objetivo es convertir reservas, mesas, intervalos de tiempo y una lista de espera en objetos con responsabilidades claras. Los reportes públicos de entrevistas han incluido el diseño de reservas de restaurantes, mientras que las guías de diseño orientado a objetos (OOD) enfatizan clarificar el alcance, listar los requisitos y luego elegir objetos e interfaces. Asume un solo restaurante, horarios fijos de apertura y un módulo en memoria; agrega persistencia y controles de concurrencia si el entrevistador lo solicita.
Qué evalúa el entrevistador
- Si separas
Restaurant,Table,Reservation,Waitlisty la política de asignación. - Si manejas correctamente los intervalos semiabiertos, la capacidad, la combinación de mesas y la liberación tras una cancelación.
- Si diseñas una máquina de estados, solicitudes idempotentes y evitas la doble asignación bajo concurrencia.
- Si las pruebas cubren los casos límite y si puedes explicar la complejidad y los puntos de extensión.
Aclaraciones que se deben hacer primero
Pregunta si los clientes eligen una mesa específica o solo el tamaño del grupo, y si las mesas se pueden combinar. Aclara la duración de la reserva y el margen de tiempo para limpieza, modificaciones, llegadas tardías, ausencias (no-shows), clientes sin reserva previa (walk-ins), orden de la lista de espera y reintentos de solicitudes. Confirma la precisión temporal, la zona horaria, si se trata de un único restaurante o varios, y si se requiere persistencia entre procesos.
Estructura para una respuesta de 30 segundos
Definiría primero un TimeRange inmutable y la máquina de estados de la reserva. AvailabilityService maneja las consultas de conflictos, TableAllocator elige según la capacidad y la política, ReservationService orquesta la creación, cancelación y notificaciones, y Waitlist gestiona a los candidatos por separado. La creación utiliza una clave de idempotencia y vuelve a verificar la disponibilidad dentro de la misma sección crítica antes de ocupar una mesa. La cancelación solo permite transiciones válidas y libera capacidad. Las pruebas cubren intervalos adyacentes, solicitudes concurrentes, cancelaciones repetidas y la promoción desde la lista de espera.
Respuesta detallada paso a paso
1. Modelar el tiempo, las mesas y los estados de la reserva
Representa una reserva mediante un intervalo semiabierto [start, end), requiriendo start < end. Dos intervalos se superponen cuando a.start < b.end && b.start < a.end. Table almacena la capacidad, el identificador y la disponibilidad; Reservation almacena al cliente, el tamaño del grupo, el intervalo, la mesa y el estado. Los estados pueden ser HELD, CONFIRMED, SEATED, CANCELLED y NO_SHOW; rechaza las transiciones inválidas.
2. Hacer que la asignación sea intercambiable
TableAllocator recibe las mesas candidatas y una solicitud. Por defecto, elige la mesa más pequeña que sea suficiente, evitando el desperdicio de espacio; otras políticas pueden priorizar mesas adyacentes o accesibilidad. El asignador no escribe reservas, manteniendo la búsqueda, la decisión y la persistencia separadas en lugar de crear una clase gigante.
3. Implementar comprobaciones de conflictos y creación
Filtra por restaurante, intervalo e índice de mesas, y luego deja que el asignador elija. La creación valida la entrada, aplica el margen de limpieza, genera una clave de idempotencia y vuelve a leer la disponibilidad dentro de un único bloqueo o transacción antes de escribir. No puede confiar en un resultado de búsqueda desactualizado. La invariante es explícita: una mesa tiene cero reservas CONFIRMED superpuestas.
create(request, key):
if idempotency.exists(key): return idempotency.result(key)
range = TimeRange(request.start, request.end + cleanupBuffer)
lock(restaurantId, range):
table = allocator.choose(availableTables(range), request.partySize)
if table is null: return WAITLISTED
reservation = Reservation.confirm(request, table, range)
store(reservation)
idempotency.save(key, reservation.id)
return reservation4. Manejar cancelación, retrasos y promociones
La cancelación permite que únicamente HELD o CONFIRMED pasen a CANCELLED; repetirla devuelve el mismo resultado y no duplica notificaciones. La llegada cambia el estado a SEATED; un tiempo de espera por política puede pasarlo a NO_SHOW y liberar la mesa. Un WaitlistMatcher consume eventos de liberación, empareja por tiempo de espera, tamaño del grupo y prioridad, y luego reutiliza la misma sección crítica de creación para que una nueva reserva no pueda asignar la mesa dos veces.
5. Probar y explicar la complejidad
Prueba que los intervalos adyacentes [19:00,20:00) y [20:00,21:00) no entren en conflicto, que se rechace una entrada invertida, que la limpieza expanda los conflictos, que una clave de idempotencia repetida no cree reservas adicionales, que la cancelación promueva como máximo una vez y que la creación concurrente tenga un único ganador. Si cada mesa almacena reservas ordenadas, la búsqueda de conflictos en una mesa es O(log n + k); con m mesas candidatas, la selección y escrituras toman aproximadamente O(m log n). Los grupos por capacidad o los índices de intervalos pueden mejorarlo.
Ejemplo de respuesta de alta calidad
Modelaría un TimeRange semiabierto inmutable, una máquina de estados de reserva explícita y una política de asignación intercambiable. AvailabilityService filtra por restaurante, tiempo y mesa; TableAllocator selecciona la mesa más pequeña suficiente; ReservationService vuelve a leer y escribe dentro de un mismo bloqueo o transacción utilizando una clave de idempotencia. La cancelación, los retrasos y el no-show liberan capacidad mediante transiciones válidas, y la asignación de la lista de espera reutiliza la misma sección crítica. Las pruebas cubren intervalos adyacentes, márgenes de limpieza, cancelaciones repetidas, creación concurrente y condiciones de carrera en la lista de espera, indicando la complejidad indexada.
Errores comunes
- Poner toda la lógica en
Restaurantcon responsabilidades poco claras y sin permitir el reemplazo de políticas. - Usar intervalos cerrados y marcar incorrectamente reservas adyacentes como conflictos.
- Buscar disponibilidad y escribir más tarde sin volver a verificar en el límite de escritura.
- Hacer que la cancelación no sea idempotente, de modo que los reintentos liberen mesas o notifiquen dos veces.
- Implementar solo las reservas de clientes e ignorar walk-ins, retrasos, no-shows y listas de espera.
- Mostrar únicamente un diagrama de clases sin invariantes, pruebas de límites o análisis de complejidad.
Preguntas de seguimiento y respuestas
¿Cómo darías soporte a mesas combinadas?
Devolviendo un conjunto ordenado de mesas en lugar de un solo tableId, agregando restricciones para la capacidad total, la conectividad y los conflictos en todo el conjunto. Mantén la interfaz del asignador y agrega una implementación de ComposableTableAllocator.
¿Cómo evitas la doble reserva entre diferentes procesos?
Moviendo la invariante de conflicto a la persistencia con un bloqueo verificable o una restricción transaccional sobre la mesa y el tiempo. Un bloqueo a nivel de aplicación puede reducir la contención, pero no puede ser la única garantía de corrección.
¿Qué sucede si un usuario hace clic en crear repetidamente?
Exige una clave de idempotencia del cliente y almacena el resultado según el ámbito del restaurante y del usuario. La misma clave devuelve la reserva original; una clave diferente sigue pasando por la comprobación de conflicto compartida. La limitación de tasa (rate limiting) no sustituye a la idempotencia de negocio.
¿Cómo optimizas un horario de cena muy concurrido?
Fragmenta (shard) por restaurante y hora. Utiliza la caché solo para sugerencias de búsqueda; la creación final se mantiene dentro de una sección crítica de fuerte consistencia. Los grupos de capacidad precalculados y los candidatos de la lista de espera aún necesitan una verificación de versión antes de escribir.
¿Qué ocurre si la notificación falla después de la cancelación?
Confirma el cambio de estado y un evento de outbox en una sola transacción; haz que los consumidores de notificaciones sean idempotentes y permitan reintentos. La liberación de la mesa no debe esperar al éxito del SMS; los fallos se envían a reintento y a una cola de mensajes no procesables (dead-letter queue) visible para los operadores.