Entrar

Estudo de caso

Como o Doorway foi feito

O Doorway é um projeto de portfólio que fiz para estudar um sistema de agendamento de ponta a ponta. Abaixo estão a arquitetura, as principais decisões e o que ainda falta.

Ver o código

Arquitetura

Como as partes se conectam

Navegador

Página do cliente e painel do negócio

HTTPS

Next.js na Vercel

Interface, tema e cliente da API

REST / JSON

FastAPI com Gunicorn

Validação, regras de agenda, limites e erros em 503 com Retry-After

asyncpg

PostgreSQL

Trava por negócio e restrição de exclusão contra sobreposição

Frontend
Next.js 16, React 19, TypeScript, Tailwind CSS 4, Motion, GSAP
Backend
Python 3.14, FastAPI, SQLAlchemy 2 assíncrono, asyncpg, Pydantic, Alembic
Banco
PostgreSQL 17 com restrição de exclusão (btree_gist) e migrações versionadas
Qualidade
pytest, Hypothesis, teste de caos, Ruff, ESLint, Prettier, GitHub Actions
Execução
Docker, Gunicorn com workers Uvicorn, Vercel

Teste ao vivo

Reservas simultâneas em produção

O botão abaixo envia 20 reservas ao mesmo tempo para o mesmo horário, no servidor de produção. Só uma é aceita e as outras recebem 409. As reservas de teste são apagadas logo depois.

Teste ao vivo

20 pessoas tentando o mesmo horário, ao mesmo tempo.

Os pedidos são numerados de 1 a 20 e disparados ao mesmo tempo pela mesma rota que um cliente usa. Só um pode ficar com o horário.

Decisões

Principais decisões técnicas

Concorrência

Reservas simultâneas

O problema
Se dois clientes escolhem o mesmo horário ao mesmo tempo, conferir a disponibilidade e só depois gravar deixa espaço para as duas reservas passarem.
A solução
Cada reserva trava a linha do negócio com SELECT ... FOR UPDATE, confere o horário e grava na mesma transação. Além disso, uma restrição de exclusão no PostgreSQL impede intervalos sobrepostos, mesmo que algum caminho do código esqueça a trava.
Detalhe
Os testes de concorrência mostraram um deadlock entre inserções sobrepostas antes da trava existir. Com ela, o teste passa em 30 execuções seguidas.

Disponibilidade

Horários calculados a cada consulta

O problema
Guardar os horários vagos numa tabela exigiria recalcular tudo sempre que o expediente ou a duração de um serviço mudasse.
A solução
Calculo os horários na hora: converto o expediente do dia para UTC e percorro os intervalos ocupados uma única vez.
Detalhe
Um teste com Hypothesis compara o resultado com uma versão de força bruta em agendas aleatórias, e outro cobre a mudança de horário de verão.

Busca

Busca por frase

O problema
No celular, passar de dia em dia procurando um horário é o passo mais demorado da reserva.
A solução
O cliente pode digitar algo como "sexta à tarde" ou "amanhã depois das 18h". Um interpretador por regras transforma a frase em dias e faixa de horário, e os horários vêm do mesmo cálculo da grade. As sugestões abaixo do campo são montadas a partir dos horários disponíveis.
Detalhe
Optei por regras em vez de um modelo de linguagem para ter respostas previsíveis e sem custo por requisição. A função é testada com casos fixos e com textos aleatórios.

Relatório

Demanda perdida

O problema
O sistema só sabia de quem conseguiu reservar. Não havia registro de quem procurou um horário e não encontrou.
A solução
Dias sem horário livre e buscas sem resultado passaram a ser registrados, separando o que caiu fora do expediente do que esbarrou na agenda cheia. O painel mostra isso por dia e hora e sugere qual horário valeria abrir.
Detalhe
O visitante é identificado por um HMAC do IP com a data, que muda todo dia. Uma restrição única evita contar várias vezes a mesma pessoa atualizando a página.

Concorrência

Reserva para várias pessoas

O problema
Fazer uma reserva por pessoa permite que a primeira entre e a segunda perca o horário para outro cliente.
A solução
Trato o grupo como um bloco único, com a soma das durações, validado sob a mesma trava e gravado numa só transação. Cada pessoa recebe o próprio link para remarcar ou cancelar.
Detalhe
Um teste envia um grupo e uma reserva simples ao mesmo tempo, disputando um horário no meio do bloco, e confere que só uma delas é aceita.

Segurança

Reservas falsas

O problema
Como a reserva não exige conta, nada impedia um robô ou uma pessoa de ocupar horários com dados inventados.
A solução
A página entrega um token assinado com o horário em que foi carregada, e a reserva só é aceita depois de alguns segundos. Há também um campo escondido para robôs, recusa de e-mails temporários, limite de reservas futuras por conexão e uma lista de clientes bloqueados pelo dono.
Detalhe
Preferi não usar captcha para não acrescentar atrito ao passo que precisa ser mais rápido.

Desempenho

Conexões com o banco

O problema
A consulta de horários fazia seis idas ao banco e, sob carga, algumas requisições esperavam mais de cinco segundos por uma conexão.
A solução
Juntei as leituras públicas em uma consulta em autocommit, coloquei uma fila na frente do pool de conexões e configurei o Gunicorn para distribuir as conexões entre os workers.
Detalhe
O script de carga está em backend/scripts/teste_carga.py.

Segurança

Entradas inválidas

O problema
Uma rota pública recebe de tudo: caracteres nulos, números enormes, corpos grandes e rajadas de pedidos.
A solução
Toda entrada tem limite de tamanho e formato, os erros de validação não repetem o valor enviado, o corpo é limitado a 64 KB e login, cadastro e reservas têm limite de tentativas guardado no banco.
Detalhe
Testes de fuzzing e um teste com 60 clientes simultâneos encontraram problemas reais, como um limite de tentativas que não funcionava com vários workers. Cada um virou um teste.

Limites

O que ainda não está pronto

  • Um profissional por negócio. Agenda por profissional fica para uma próxima versão.
  • Não há lembretes por e-mail.
  • Os limites de tentativas ficam no PostgreSQL. Com mais volume, faria sentido usar Redis.
  • A integração com o Google Agenda é só de ida, por link e arquivo .ics. A sincronização nos dois sentidos exige aprovação do Google.
Mais detalhes no README