Duas pessoas reservaram o mesmo horário: o problema que um CRUD simples não resolve
Um sistema de agendamento pode passar por todos os testes manuais e ainda confirmar duas reservas para o mesmo profissional, no mesmo horário.
Não precisa ter milhões de usuários. Duas requisições executadas no momento certo já são suficientes.
Vamos usar um cenário didático: um profissional atende uma pessoa por vez, e cada atendimento ocupa um horário previamente definido. Ana e Bruno tentam reservar a mesma vaga das 14h.
O fluxo parece correto
Uma primeira implementação costuma seguir esta sequência:
consultar se a vaga está disponível
se estiver ocupada:
informar conflito
se estiver livre:
criar reserva
confirmar para o cliente
Sozinho, esse fluxo parece funcionar. Ana reserva; quando Bruno tenta, o sistema encontra a reserva e recusa a segunda solicitação.
O problema aparece quando as duas operações se sobrepõem:
| Momento | Requisição de Ana | Requisição de Bruno |
|---|---|---|
| 1 | Consulta: vaga livre | |
| 2 | Consulta: vaga livre | |
| 3 | Grava a reserva | |
| 4 | Grava a reserva | |
| 5 | Recebe confirmação | Recebe confirmação |
Cada requisição tomou uma decisão com base em uma informação que deixou de ser suficiente antes da gravação. Essa é uma condição de corrida.
A regra precisa ser mais precisa
“Verificar disponibilidade” descreve uma ação. Não define, sozinha, a garantia que o sistema precisa oferecer.
Uma regra melhor para este exemplo é:
Para cada vaga previamente definida, pode existir no máximo uma reserva ativa.
Essa frase muda a implementação. Agora temos algo que precisa continuar verdadeiro mesmo quando duas operações acontecem juntas.
Também precisamos explicitar o contexto: são vagas fixas ou intervalos livres? Um cancelamento libera a vaga? Há capacidade para mais de uma pessoa? O profissional atende em mais de uma unidade?
As respostas fazem parte do desenho do sistema.
Uma solução para vagas fixas
Se cada vaga já existe e tem um identificador próprio, podemos fazer o banco impedir que duas reservas ativas usem a mesma vaga.
Exemplo didático em PostgreSQL:
CREATE TABLE reservas (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
vaga_id bigint NOT NULL,
cliente_id bigint NOT NULL,
cancelada_em timestamptz
);
CREATE UNIQUE INDEX uma_reserva_ativa_por_vaga
ON reservas (vaga_id)
WHERE cancelada_em IS NULL;
O índice parcial aplica a unicidade somente às reservas ativas. A consulta prévia pode continuar existindo para orientar o usuário, mas a gravação passa a ter uma proteção independente dela.
O esquema foi reduzido para mostrar a regra; um sistema completo também precisa das referências para vagas e clientes, permissões e demais validações. A sintaxe é ilustrativa e não foi executada durante a preparação deste artigo.
Na aplicação, o conflito dessa restrição deve virar uma resposta compreensível: “Essa vaga acabou de ser reservada. Escolha outra.” Em uma API HTTP, eu usaria 409 Conflict para esse caso de negócio. Outros erros de banco devem ser tratados conforme sua causa; não basta transformar qualquer exceção em “horário ocupado”.
O cliente só recebe confirmação depois que a transação confirma a reserva.
E quando os horários têm duração variável?
Aqui o modelo anterior já não basta. Duas reservas podem começar em horários diferentes e ainda se sobrepor: uma das 14h às 15h e outra das 14h30 às 15h30.
No PostgreSQL, tipos de intervalo e restrições de exclusão permitem representar e impedir sobreposições para um mesmo recurso. Essa é outra modelagem, que precisa considerar o profissional ou recurso reservado, cancelamentos e limites do intervalo. A documentação oficial mostra exemplos dessa abordagem.
Minha escolha seria começar pela regra de capacidade e duração, antes de selecionar o mecanismo de controle.
“Coloquei em uma transação” não encerra a discussão
Transações e controle de concorrência precisam ser avaliados em conjunto. No isolamento padrão Read Committed do PostgreSQL, duas consultas podem observar a ausência de uma reserva antes de qualquer uma das gravações ser confirmada.
Uma transação não cria automaticamente a restrição que o domínio exige. Índices, restrições, bloqueios ou isolamento mais forte podem fazer parte da solução, conforme o modelo. Usar Serializable, por exemplo, exige tratar falhas de serialização com repetição da transação completa.
Como eu verificaria a garantia
O teste importante não é apenas reservar e tentar novamente. É coordenar duas requisições para disputarem a mesma vaga.
Eu verificaria:
- Somente uma reserva ativa persiste para a vaga.
- A requisição vencedora recebe confirmação depois do commit.
- A outra recebe um conflito de negócio compreensível.
- Reservas de vagas diferentes continuam funcionando.
- Cancelar uma reserva libera a vaga conforme a regra estabelecida.
- A proteção continua válida com mais de uma instância da aplicação.
Também separaria esse problema da idempotência. Repetir uma solicitação após uma falha de rede não deve criar uma reserva extra. Isso exige uma política própria para reconhecer a repetição; não substitui a regra de exclusividade da vaga.
A decisão arquitetural está no comportamento
O ponto não é adicionar ferramentas ao projeto. É definir qual estado nunca pode existir e escolher onde essa garantia será sustentada.
A mesma pergunta aparece na última unidade de um estoque, no limite de inscrições de um evento e na capacidade de uma sala.
No seu projeto, qual regra parece correta quando as operações são executadas uma de cada vez, mas ainda não foi verificada com concorrência?
Sobre o autor: sou Marcos Chaves, autor de Arquitetura de Software para Iniciantes — Como pensar, decidir e evoluir sistemas sem complicar. No livro, uso o projeto Agenda Fácil para explorar decisões sobre dados, APIs, segurança e evolução. Este artigo é um conteúdo complementar; o exemplo SQL acima foi elaborado para esta publicação.
Também escrevi Construa sua Primeira API Profissional com Java 21, que acompanha a construção do Nexus CRM. Os dois títulos estão disponíveis na Amazon: