Design clássico · Sênior
Reserva de hotel (Booking/Airbnb)
Projeto de entrevista clássico. Pontos centrais: reservas e concorrência.
Resposta curta
Projeto de entrevista clássico. Pontos centrais: reservas e concorrência. O exercício clássico pede o sistema de reservas de uma rede de hotéis: mostrar a página do hotel e a de detalhe do quarto, reservar um quarto, cancelar e administrar os dados dos hotéis.
01 · CONTEXTO
Pouca vazão, muita disputa
O exercício clássico pede o sistema de reservas de uma rede de hotéis: mostrar a página do hotel e a de detalhe do quarto, reservar um quarto, cancelar e administrar os dados dos hotéis. O cliente paga o valor integral no momento da reserva, e a busca de hotéis fica fora do escopo. Há ainda um requisito de negócio que complica o estoque: a rede aceita overbooking de dez por cento, isto é, vender mais quartos do que existem, na expectativa de que alguns clientes cancelem.
A escala de referência é uma rede com cinco mil hotéis e um milhão de quartos, com 70% de ocupação e estadia média de três dias. Isso dá cerca de 240.000 reservas por dia, ou algo como três reservas por segundo. Descendo o funil, a página de confirmação recebe cerca de 30 consultas por segundo e a de detalhe, cerca de 300.
O requisito não funcional que pesa é a alta concorrência: em alta temporada ou em grandes eventos, hotéis populares recebem muitos clientes tentando reservar o mesmo quarto. A latência, por outro lado, é moderada: aceita-se que o sistema leve alguns segundos para processar uma reserva. Por isso este caso é, no fundo, um problema de escrita concorrente sobre um estoque pequeno.
02 · MODELO
Reservar um tipo de quarto, não um quarto
Uma decisão de modelagem muda todo o resto: a reserva é feita no nível do tipo de quarto, e não de um quarto específico. O cliente reserva, por exemplo, um quarto duplo de um hotel para certas datas, e o hotel atribui o quarto físico depois. A tabela de inventário guarda, por hotel e tipo de quarto, quantos quartos existem e quantos já foram reservados, com uma linha por data, o que torna simples consultar e atualizar um intervalo de datas. Cada reserva, por sua vez, percorre estados: pendente, paga, reembolsada, cancelada ou rejeitada. Veja a modelagem de dados em ação no fluxo abaixo.
O cliente abre o site ou o aplicativo da rede; a requisição entra pela API do comerciante, que a encaminha ao serviço certo.
As páginas de hotel e de quarto vêm do serviço de hotéis, que lê o catálogo. Esses dados mudam pouco e não disputam estoque.
Ao escolher tipo de quarto e datas, o serviço de reservas gera um pedido pendente com um identificador único, que volta ao cliente para revisão.
Ao confirmar, o serviço grava a reserva e atualiza o inventário no mesmo banco relacional, numa só transação.
Se o cliente cancelar, a reserva muda de estado e a quantidade reservada do inventário diminui, devolvendo o quarto ao estoque.
03 · DUPLO CLIQUE
O mesmo usuário pedindo duas vezes
A venda dupla tem duas origens diferentes. A primeira é o mesmo usuário enviando a reserva mais de uma vez, por exemplo clicando duas vezes no botão. A segunda é a disputa de vários usuários pelo mesmo quarto, tratada na próxima seção. Bloquear o botão no cliente ajuda, mas não é confiável: o usuário pode, por exemplo, desativar o JavaScript e contornar a checagem.
A defesa que vale é no servidor: uma API idempotente, que produz o mesmo resultado não importa quantas vezes seja chamada. No desenho do livro, o serviço de reservas gera um identificador único ao criar o pedido, o cliente o envia ao confirmar, e ele é a chave primária da tabela de reservas. Se o usuário clicar de novo, a segunda gravação viola a restrição de unicidade e nenhuma reserva duplicada nasce.
04 · ÚLTIMO QUARTO
Dois usuários, um quarto
Suponha que reste um único quarto e que o banco não use o nível de isolamento serializável. Duas transações leem o inventário, ambas concluem que ainda há vaga, e cada uma grava a sua reserva. Como as mudanças de uma transação só ficam visíveis às outras após o commit, a segunda ainda enxerga o estado antigo. O resultado é o sistema permitir que os dois usuários reservem, mesmo havendo apenas um quarto. Resolver isso exige, em geral, algum tipo de trava ou verificação na hora de gravar.
Dois usuários pedem, quase ao mesmo tempo, o último quarto de um tipo, para as mesmas datas.
Cada transação lê o inventário e vê que ainda há um quarto livre. Nenhuma enxerga a outra, porque nada foi confirmado ainda.
Sem nenhuma proteção, as duas gravam a reserva e confirmam. O estoque diz que o quarto foi vendido, mas há dois hóspedes com direito a ele.
Com uma das proteções ativa, uma transação passa e a outra é recusada ou espera. O usuário recusado recebe um aviso de que o quarto acabou de ser vendido.
No bloqueio pessimista, a transação trava as linhas lidas, e as demais esperam até que ela termine. É simples e serializa as atualizações, mas pode causar deadlock quando vários recursos são travados e prejudica o desempenho quando as transações são longas. Por esses limites, o livro não o recomenda para este sistema.
No bloqueio otimista, uma coluna de versão acompanha a linha: ao gravar, a aplicação incrementa a versão e o banco confere se o novo valor é o esperado; se não for, a transação aborta. Sem contenção ele é mais rápido, porque nada fica travado. Sob contenção forte, só um dos clientes consegue gravar e os outros recebem falha de versão e precisam tentar de novo. Como a taxa de reservas costuma ser baixa, o livro o considera uma boa opção aqui.
A terceira opção é uma restrição no próprio banco, que rejeita qualquer estado em que o total reservado ultrapasse o inventário. É parecida com o otimista: fácil de implementar e adequada a pouca contenção, mas sob disputa forte gera muitas falhas. O livro a considera mais uma boa opção para o problema.
| Abordagem | Como evita a venda dupla | Limite principal |
|---|---|---|
| Bloqueio pessimista | Trava as linhas lidas; as outras transações esperam | Deadlocks e perda de desempenho com transações longas |
| Bloqueio otimista | Coluna de versão; grava só se a versão não mudou | Muitas novas tentativas quando há disputa forte |
| Restrição no banco | O banco recusa estoque reservado acima do inventário | Muitas falhas sob disputa forte; regra fora do controle de versão da aplicação |
05 · ESCALA
E se fosse um site de viagens inteiro?
Uma pergunta de aprofundamento comum: e se o sistema atendesse não uma rede, mas um grande site de viagens, com uma carga mil vezes maior? Os serviços são sem estado e escalam adicionando servidores. O banco, que concentra todo o estado, é o gargalo e não escala só com mais instâncias.
A saída é o particionamento do banco. Como quase toda consulta filtra por hotel, a chave natural é o identificador do hotel: toda a história de um hotel vive no mesmo fragmento, e uma reserva toca um único banco. No exemplo do livro, com trinta mil consultas por segundo espalhadas por dezesseis fragmentos, cada um recebe cerca de 1.875 por segundo, dentro da capacidade de um servidor MySQL.
A consulta de disponibilidade, de longe a operação mais frequente, é respondida pelo cache de inventário, sem tocar no banco.
A reserva vai ao fragmento escolhido pelo hash do identificador do hotel e é validada pelo banco.
O banco é atualizado primeiro; um processo de captura de mudanças lê o que mudou e o envia ao cache de forma assíncrona.
O cache converge para o estado do banco, depois de um pequeno intervalo.
Nesse intervalo o cache pode mostrar um quarto que o banco já vendeu. A reserva chega ao banco, que faz a validação final e recusa; o usuário vê que o quarto acabou de ser vendido.
A leitura de inventário é uma ordem de grandeza mais frequente que a escrita, então um cache na frente do banco reduz muito a carga. O livro sugere o Redis, porque o TTL e o descarte do menos usado ajudam a aproveitar a memória. Mesmo quando o Redis mostra vaga, a reserva é conferida no banco, que é a fonte da verdade do estoque.
O custo é a consistência entre banco e cache. O banco é atualizado primeiro e a mudança segue para o cache de forma assíncrona, pelo código da aplicação ou por captura de mudanças (CDC), mecanismo que lê as alterações do banco e as aplica em outro sistema. A defasagem é aceitável, segundo o livro, desde que o banco faça a validação final do estoque.
06 · MICROSSERVIÇOS
Consistência entre serviços
Num monólito, várias operações cabem numa única transação do banco. Na arquitetura de microsserviços, cada serviço tem o seu banco, e uma operação logicamente atômica pode atravessar vários serviços; assim, não há uma transação única para garantir a consistência.
O livro cita duas saídas conhecidas para as transações distribuídas. O commit em duas fases (2PC) é bloqueante: a falha de um nó interrompe o progresso até ele voltar. Já a saga é uma sequência de transações locais; se um passo falha, executam-se transações compensatórias para desfazer os passos anteriores.
A escolha do livro para este problema foi pragmática: uma abordagem híbrida, em que o serviço de reservas cuida das APIs de reserva e de inventário, mantendo as duas tabelas no mesmo banco relacional e aproveitando as propriedades ACID. Decidir se a complexidade extra de distribuir vale a pena é, nas palavras do livro, uma escolha do arquiteto.
Perguntas frequentes
O que é Reserva de hotel (Booking/Airbnb)?
Projeto de entrevista clássico. Pontos centrais: reservas e concorrência.
O que o design de Reserva de hotel (Booking/Airbnb) cobre?
Ele está organizado nestas seções: Pouca vazão, muita disputa, Reservar um tipo de quarto, não um quarto, O mesmo usuário pedindo duas vezes, Dois usuários, um quarto, E se fosse um site de viagens inteiro? e Consistência entre serviços.
Quanto tempo leva Reserva de hotel (Booking/Airbnb) e qual é o nível?
Leva cerca de 16 minutos para ler e tem nível Sênior.
Agora projete você mesmo
Rode este caso como uma entrevista na escola de System Design: você toma as decisões, o tutor as questiona e você vê a nota do seu desenho.