root@construct:~/logs/construindo-sistemas-de-pagamento-que-nao-perdem-dinheiro$
<-- voltar para /logs
2026-01-16//LOG

Construindo Sistemas de Pagamento Que Não Perdem Dinheiro

Faz um tempo que construo infra de processamento de pagamento na Yooga, num sistema POS pra restaurante. Cada transação mexe com dinheiro de verdade, e uma falha tem consequência pra quem tá usando. Aprendi umas coisas sobre evitar essas perdas em produção.

Regra um: idempotência não é opcional

Toda operação de pagamento precisa de chave de idempotência. Mesmo que tu ache que teu sistema nunca vai retentar um request, a rede soluça, o cara dá double tap, ou o load balancer dá timeout e tenta de novo. Sem idempotência no endpoint, tu vai processar cobrança duplicada.

A gente usa chave composta: ID do merchant + UUID gerado pelo client + tipo de operação. Isso é gravado antes de falar com o processador de pagamento. Se a gente vê a mesma chave duas vezes, retorna o resultado cacheado da primeira tentativa. Isso evita uma segunda cobrança.

Regra dois: race condition vai te achar

Duas requests concorrentes conseguiam ler o mesmo saldo, verificar fundos suficientes e debitar. O merchant gastava mais do que tinha. Descobrimos essa race condition quando um restaurante de teste processou saldo negativo no rush do almoço.

Resolvemos com locking pessimista no check de saldo: SELECT FOR UPDATE na row da conta, verifica fundos, debita, commit. Serializa pagamento concorrente pra mesma conta e fica um pouco mais lento. Eu aceito esse custo; em sistema de pagamento, corretude ganha de performance toda vez.

Mas esse era o race condition fácil. O difícil foi com transação concorrente no terminal de cartão. Dois POS, mesmo merchant, os dois batendo no processador ao mesmo tempo. O processador retorna sucesso pros dois, mas nosso webhook handler processa fora de ordem e a segunda transação sobrescreve o status da primeira. A gente perdeu visibilidade de pagamento concluído.

Pra isso usamos event sourcing nas mudanças de estado da transação. Cada atualização de status é um evento append-only com número de sequência. A gente reconstrói o estado atual pelo log, sem sobrescrever nada e com trilha de auditoria completa. Quando um evento chega fora de ordem, resequencia pelo timestamp do processador.

Regra três: transações duplicadas

Esse foi o bug mais caro que já shipei pra produção.

A gente tinha mecanismo de retry de webhook. Quando o processador mandava confirmação de pagamento e a gente não confirmava (HTTP 200), eles retentavam. Normal. Só que nosso handler não checava se a transação já tinha sido registrada. Então cada retry criava um novo registro de transação no nosso sistema. PQP.

Numa sexta à noite lotada, nosso endpoint de webhook caiu por uns 90 segundos por causa de um deploy. O processador enfileirou retry. Quando voltou, tomamos uma rajada de webhook de retry. Cada um criou transação duplicada. Isso gerou lançamentos duplicados nos merchants afetados.

Pegamos em duas horas por causa dos alertas de reconciliação. Mas aquelas duas horas foram as mais longas da minha carreira. Tivemos que reverter manualmente cada duplicata, ligar pra cada merchant afetado, e explicar o que aconteceu.

O fix era constrangedor de simples. Checar o ID de transação do processador contra nossos registros antes de criar entrada nova. Se existe, confirma e pula. Fix de cinco linhas pra impedir lançamentos duplicados.

Regra quatro: reconciliação é teu colchão de segurança

Todo dia, às 3h da manhã, rodamos um job de reconciliação. Ele puxa as transações das últimas 24 horas no nosso sistema e no processador, compara as duas listas e dispara alerta pra qualquer divergência.

Isso pegou bug que mais nada ia achar. Falha silenciosa onde a gente registrou pagamento mas o processador na real recusou. Edge case onde reembolso parcial se perdeu. Bug de timezone onde transação aparecia em dia diferente no nosso sistema vs no processador.

Quase ninguém fala de reconciliação no LinkedIn, mas pra mim ela é a peça mais importante da infra de pagamentos. Builda antes de processar tua primeira transação real.

Regra cinco: nunca confia no client

O terminal POS manda o valor a cobrar, mas a gente recalcula no servidor a partir dos itens do pedido. Tivemos um incidente com APK modificado num terminal comprometido mandando valor menor que o total real. O merchant tava dando desconto que não pretendia.

Cálculo de valor server-side a partir da fonte de verdade (o pedido no nosso banco) é inegociável. Valor do terminal é só pra exibição.

O código de pagamento costuma ser direto; a dificuldade não tá nos algoritmos. Tá nos edge cases que custam dinheiro. Numa plataforma de blog, uma race condition gera post duplicado. Num sistema de pagamento, alguém perde dinheiro. Isso muda como tu pensa sobre cada linha.

Por isso eu começo pela idempotência, uso locking agressivamente e reconcilio tudo. Valor que vem do client precisa ser conferido no servidor. E, pelo amor de Deus, testa teus webhook handlers com entrega duplicada antes de deployar pra produção.

The Broad Way | Kinho.dev