Pular para o conteúdo
caua.tolentino
Todos os cases
Investigação

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.

  1. Cliente finaliza a compra
  2. Trava por carrinho é criada
  3. Criação do pedido falha por regra de negócio
  4. Trava liberada e erro exibido
  5. 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.

Serviço de checkout
$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.