DEV Community

Cover image for Cobrança recorrente em produção: o que ninguém te conta antes da primeira cobrança duplicada
Felipe Ricarte
Felipe Ricarte

Posted on

Cobrança recorrente em produção: o que ninguém te conta antes da primeira cobrança duplicada

Implementar cobrança recorrente parece um problema resolvido. Você integra um gateway, agenda um job pra rodar todo dia e cobra quem vence hoje. Em uma tarde está "funcionando".

Aí o sistema vai pra produção, você escala pra duas instâncias, e numa madrugada o mesmo cliente é cobrado duas vezes. Ou pior: o pagamento confirma, mas o módulo que libera o acesso nunca fica sabendo — e o cliente paga sem receber.

A cobrança em si é a parte fácil. O que separa um protótipo de um billing de produção são quatro detalhes que quase ninguém trata no começo.

1. Concorrência: o job que roda duas vezes

O cenário clássico: um @Scheduled que busca assinaturas vencidas e cobra cada uma. Com uma instância, funciona. Com duas (deploy sem downtime, autoscaling), as duas leem a mesma assinatura no mesmo segundo e cobram em dobro.

A solução não é "garantir uma instância" — é tornar a leitura segura sob concorrência. No PostgreSQL:

SELECT * FROM assinatura
WHERE proxima_cobranca <= now() AND status = 'ATIVA'
FOR UPDATE SKIP LOCKED
LIMIT 50;
Enter fullscreen mode Exit fullscreen mode


{data-source-line="212"}

FOR UPDATE SKIP LOCKED faz cada instância travar e processar um lote disjunto: ninguém espera, ninguém duplica. Escala horizontalmente sem coordenação externa.

2. Retry determinístico: falhar não pode ser improviso

Cartão recusado acontece o tempo todo. A pergunta é: quantas vezes tentar, com qual intervalo, e quando desistir? Se isso for "ad hoc", você terá clientes suspensos cedo demais e outros cobrados pra sempre.

Trate a falha como uma máquina de estados explícita:

  • 1ª falha → reagenda +15 min
  • 2ª falha → +60 min
  • 3ª falha → suspende a assinatura e desliga a renovação

Cada tentativa fica registrada e auditável. O comportamento é previsível — para você e para o cliente.

3. Outbox: o evento que não pode se perder

O erro mais caro: cobrar com sucesso e, na sequência, publicar o evento PagamentoConfirmado num broker. Se a aplicação morre entre as duas operações, o pagamento aconteceu mas o resto do sistema nunca soube. Dinheiro entrou, acesso não foi liberado.

O padrão Outbox resolve: grave o evento na mesma transação da cobrança, numa tabela outbox. Um publicador lê essa tabela e entrega ao broker com retry/backoff, marcando como DEAD ao estourar o limite.

BEGIN;
  UPDATE assinatura SET status = 'ATIVA', proxima_cobranca = ... WHERE id = ?;
  INSERT INTO outbox (tipo, payload, status) VALUES ('PagamentoConfirmado', ?, 'PENDENTE');
COMMIT;
Enter fullscreen mode Exit fullscreen mode


{data-source-line="239"}

Ou a cobrança e o evento são persistidos, ou nenhum dos dois. Sem estado inconsistente.

4. Idempotência no consumidor

O outbox garante entrega pelo menos uma vez — então o mesmo evento pode chegar duas vezes. O consumidor precisa ser idempotente. A forma mais simples e robusta é uma constraint única na chave do evento:

ALTER TABLE evento_processado ADD CONSTRAINT uq_evento UNIQUE (evento_id);
Enter fullscreen mode Exit fullscreen mode


{data-source-line="249"}

Processou de novo? A inserção viola a constraint e você ignora com segurança. Sem efeito colateral duplicado.

O encanamento toma mais tempo que o produto

Nada disso é "difícil" isoladamente. O problema é que são semanas de encanamento — concorrência, retry, outbox, idempotência, métricas, testes com containers — antes de escrever uma linha da regra de negócio que importa pro seu SaaS. E é justamente a parte onde um bug custa cliente e reputação.


Atalho

Empacotei exatamente essa camada num starter kit em Java 21 + Spring Boot 3: renovação automática, retry determinístico, outbox com idempotência, suspensão por inadimplência, métricas Prometheus e testes com Testcontainers — tudo documentado e rodando via Docker Compose.

É o AssinaFlow. Se você vai construir cobrança recorrente, ele economiza as semanas chatas e te dá uma base que já nasce pronta pra produção.

👉 Conheça o AssinaFlow

Dúvidas sobre alguma das decisões acima? Comenta aí — respondo todas.

Top comments (0)