Trava de checkout que não era liberada após erro
Quem errava algo no checkout ficava até 60 segundos sem conseguir comprar, em todas as lojas. Encontrei a causa na trava contra pedidos duplicados e defini em quais erros ela pode ser liberada, acabando com o bloqueio sem abrir espaço para cobrança dupla.
- Papel
- Desenvolvedor responsável, do diagnóstico à entrega
- Contexto
- Plataforma de e-commerce multi-tenant
- Stack
- PHP, Symfony, Redis
- Time
- Solo
- Período
- 2026
Case anonimizado: nomes de empresa, cliente e pessoas foram omitidos ou trocados.
01. Situação
Um erro no checkout bloqueava a nova tentativa por até um minuto
Ao finalizar a compra, qualquer erro de validação, como frete não escolhido, cartão recusado ou estoque insuficiente, mostrava a mensagem certa. Mas, se o cliente corrigisse o problema e tentasse de novo logo em seguida, recebia outra mensagem: "já existe uma compra em andamento". Não havia compra nenhuma em andamento.
O problema valia para todas as lojas da plataforma. O único contorno era esperar cerca de um minuto, o que, no checkout, muitas vezes significa perder a venda. Fiz a análise e abri o chamado já com a causa identificada.
- tempo máximo de bloqueio após um erro
- 60 s
- lojas afetadas
- todas
02. Investigação
A criação do pedido ficava fora do bloco que libera a trava
Para impedir pedidos duplicados por duplo clique, o checkout cria uma trava no Redis por carrinho, com validade de 60 segundos. A trava é liberada em dois casos: quando o pedido termina com sucesso ou quando um erro é capturado pelo bloco que controla a transação do banco.
Segui o fluxo linha a linha, procurando todo ponto em que a trava é criada e liberada. Encontrei a chamada que cria o pedido antes desse bloco. Quando ela lançava um erro de domínio, a exceção saía do método sem passar pelo código que libera a trava. A próxima tentativa encontrava a trava ativa e era recusada.
A mensagem aparecia mesmo com uma única tentativa nova, feita depois de corrigir o erro. Descartada.
03. Solução
Liberar a trava só quando nenhum pagamento foi processado
A correção mais rápida seria liberar a trava em qualquer erro. Não fiz isso. A trava existe para impedir cobrança duplicada, então a pergunta que eu precisava responder era: em quais erros é seguro liberar?
Defini a regra: só nos erros de domínio do pedido, que garantem que nada foi cobrado. Envolvi a criação do pedido em um bloco próprio, que captura essa exceção, libera a trava e relança o erro. A mensagem ao cliente continua a mesma.
- Cliente finaliza a compra
- Trava por carrinho é criada
- Criação do pedido falha por regra de negócio
- Trava liberada e erro exibido
- se a falha vier depois do pagamento aprovado: a trava é mantida pelos 60 s
Escolha
Capturar só a exceção de domínio do pedido
Erros de frete, estoque e pagamento recusado garantem que nada foi cobrado, então é seguro liberar a trava.
Cuidado
Não capturar qualquer exceção
Se o pagamento fosse aprovado e uma falha de infraestrutura viesse depois, liberar a trava permitiria uma segunda cobrança.
Trade-off
Dois blocos separados
Mover a chamada para dentro do bloco da transação faria o rollback rodar sem transação aberta, o que gera outro erro. Preferi um bloco próprio.
$this->startTransactionLock($cart);
$order = $this->createOrder($request);
try {
// transação do banco; o catch libera a trava
} catch (\Exception $e) {
$this->releaseLock($e);
throw $e;
}
04. Resultado
O cliente corrige o erro e compra na hora
Depois de um erro de validação, o cliente pode tentar de novo na mesma hora, em todas as lojas. O duplo clique continua bloqueado, e falhas que acontecem depois do pagamento mantêm a trava, como antes.
O valor da mudança está na regra que defini e documentei: quais erros podem liberar a trava e quais não podem. Também identifiquei que o serviço de pedidos para várias lojas tem a mesma estrutura e deixei isso apontado para o time.