O problema mais difícil deste projeto não é nenhum dos CRUDs — é garantir que dois hóspedes
nunca consigam reservar o mesmo imóvel para datas que se sobrepõem, mesmo sob concorrência,
sem pagar o preço de um lock pessimista de longa duração. Este documento narra a solução
implementada em apps/property-management, que foi construída em cinco decisões sequenciais
(cada uma registrada como ADR) mais um mecanismo de aplicação (Listing) que as costura.
Se você quer o registro formal de cada decisão isolada, com alternativas descartadas, veja
apps/property-management/adr/ADR-0001 a ADR-0005. Este documento é o "como tudo se encaixa",
não substitui os ADRs.
Listing (anúncio de um imóvel) precisa saber, para qualquer intervalo de datas
solicitado, se está disponível.RESERVED), holds temporários
(HOLD, criados enquanto um hóspede está no processo de reserva mas ainda não confirmou) e
bloqueios manuais do anfitrião (BLOCKED).HOLD/RESERVED para o mesmo
intervalo ao mesmo tempo — a garantia de não-sobreposição não pode depender só de uma
checagem na camada de aplicação antes do insert (isso é uma race condition clássica de
check-then-act).| ADR | Decisão | Por quê |
|---|---|---|
| 0001 | Disponibilidade é modelada como intervalos de datas (start_date/end_date), não uma linha por dia | Uma linha por dia cresce linearmente (365 registros/imóvel/ano); intervalos escalam para milhões de imóveis |
| 0002 | Só se persiste o que é especial (RESERVED, HOLD, BLOCKED); AVAILABLE é inferido como o gap entre intervalos | Menos escrita, menos registros, HOLD pode simplesmente ser removido sem reconstruir o resto |
| 0003 | Reservas só podem ser criadas até 12 meses à frente (janela deslizante) | Não é limitação técnica — é controle deliberado de crescimento de dados e previsibilidade, revisável se o produto exigir mais |
| 0004 | Verificação de sobreposição é transacional e otimista (checa, insere dentro da mesma transação), não lock pessimista prolongado |
A lógica dessas decisões é acumulativa: 0001 e 0002 definem o que é armazenado; 0003 limita até onde no tempo; 0004 e 0005 definem como a não-sobreposição é garantida sob concorrência.
O aggregate Listing
(apps/property-management/src/modules/property-module/domain/models/listing.ts) concentra o
comportamento:
isAvailableFor(period, now) — verifica se um período não colide com nenhum intervalo
HOLD/RESERVED vigente.placeHold(...) — cria um intervalo HOLD após checar disponibilidade (check-then-insert,
conforme ADR-0004).confirmReservation(...) — promove um HOLD para RESERVED.releaseInterval(...) — libera um HOLD ou RESERVED (ex: cancelamento).blockPeriod(...) — cria um BLOCKED (uso do anfitrião, fora do fluxo de reserva).cleanupExpiredHolds(now) — remove HOLDs expirados antes de qualquer nova inserção,
conforme item 3 da decisão do ADR-0005.Regras de negócio auxiliares vivem como objetos dedicados em domain/rules/:
interval-within-sliding-window-rule.ts (aplica a janela de 12 meses do ADR-0003) e, no
booking-module, reservation-min-duration-rule.ts e cancellation-window-rule.ts.
Vale ser honesto aqui, porque é uma diferença real neste momento do projeto:
Implementado e testado (domínio/aplicação):
Listing, com testes unitários e o teste de integração
drizzle-listing-repository.e2e-spec.ts, que inclui o caso
"should reject overlapping intervals via exclusion constraint".Pendente (infraestrutura — rastreado em PENDENCIAS-INFRA.md):
placeHold numa transação no repositório concreto, com
retry em caso de conflito.HOLDs expirados (hoje a limpeza só acontece de forma
"lazy", quando cleanupExpiredHolds é chamado antes de um novo placeHold).Ou seja: a garantia estrutural do ADR-0005 (constraint de exclusão + READ COMMITTED) está
desenhada e o teste de integração já a exercita, mas a fiação completa em produção
(transação + retry + mapeamento de erro + cleanup job) ainda não está fechada.
O diagrama abaixo é o nível 3/dinâmico do C4 para o caso de uso mais interessante do sistema —
"hóspede tenta reservar um listing". Ele mostra a colaboração entre booking-module e
property-module dentro do mesmo container (property-management), via a interface
interna PropertyModuleInterface — não é uma chamada de rede entre serviços.
Nota de honestidade: a seta inicial Guest → Nginx está marcada como "ainda não roteado"
porque booking-module não tem controllers HTTP hoje (ver C4 Container).
O fluxo a partir de CreateReservationUseCase é real e testado
(create-reservation-use-case.spec.ts e o e2e do repositório de listing); falta só a camada de
transporte para o hóspede chegar até ele.
| Lock pessimista mata throughput sob concorrência real; a alternativa escolhida escala melhor e é compatível com múltiplas instâncias |
| 0005 | A garantia final é uma constraint estrutural de exclusão no Postgres + isolamento READ COMMITTED, com expiração de HOLD tratada pela aplicação | Deixar a garantia só na aplicação é vulnerável a race condition; SERIALIZABLE sem constraint tem custo de performance e mais aborts. A combinação escolhida separa "integridade" (banco) de "regra temporal" (aplicação) |