Pular para o conteúdo

Arquitetura de software · Estudos de caso · 9 min de leitura

Estudo de caso: um sistema de pagamentos

Cobrar exatamente uma vez, nunca perder um centavo e saber explicar cada movimento: idempotência, máquina de estados, webhooks, razão contábil de partidas dobradas e conciliação.

App / checkout → Serviço de pagamentos1Serviço de pagamentos → PagamentosServiço de pagamentos → PSP / banco: cobra ⇄ webhookcobra ⇄ webhook2Serviço de pagamentos → Razão: lança3Conciliação → Razão: confereconfereConciliação → PSP / banco: relatóriorelatórioApp / checkout — Idempotency-KeyApp / checkoutIdempotency-KeyServiço de pagamentos — máquina de estadosServiço de pagamentosmáquina de estadosPSP / banco — cartão, PixPSP / bancocartão, PixPagamentos — estado + chavePagamentosestado + chaveRazão — partidas dobradasRazãopartidas dobradasConciliação — diáriaConciliaçãodiária

Arraste para o lado para ver o diagrama inteiro.

A resposta síncrona do PSP pode se perder; o webhook e a conciliação garantem que o estado final sempre chega e confere.

Requisitos

  • Exatamente uma cobrança por compra, mesmo com cliques duplos, timeouts e retries.
  • Nada some: todo centavo tem origem e destino registrados.
  • Auditável: dá para explicar qualquer saldo a qualquer momento.
  • Segurança: dados de cartão nunca passam pelos seus servidores (tokenização do PSP), o que reduz muito o escopo do PCI DSS.

Idempotência e estados

O checkout gera uma chave de idempotência por tentativa de compra; o serviço grava a chave com o pagamento numa restrição única, então a segunda requisição devolve o mesmo resultado em vez de cobrar de novo. O pagamento é uma máquina de estados explícita — criado → pendente → autorizado → capturado, com saídas para falhou, cancelado e estornado — e só transições válidas são aceitas (um pagamento capturado não volta a pendente).

Webhooks e Pix

Muita coisa termina de forma assíncrona: 3-D Secure, boleto, Pix. No Pix com QR dinâmico, o sistema cria a cobrança com um txid, mostra o QR e espera o webhook do banco avisando o pagamento. Webhooks chegam repetidos e fora de ordem: valide a assinatura, grave o ID do evento para ignorar duplicatas e aplique só transições válidas. Se o webhook não vier, um processo consulta o status no PSP depois de um tempo.

Razão contábil e conciliação

Saldos não são um número que se atualiza: são a soma de lançamentos imutáveis em partidas dobradas — cada movimento debita uma conta e credita outra no mesmo valor, então a soma de tudo é sempre zero. Erros se corrigem com novos lançamentos, nunca editando os antigos. Valores em centavos inteiros (nunca float). Todo dia, a conciliação compara o razão com o relatório do PSP e do banco; qualquer diferença vira um alerta para investigar.

No código

✓ ExemploTypeScript
// Webhook do PSP: assinatura válida, evento processado uma única vez, só transições permitidas.
const NEXT: Record<string, string[]> = {
  pendente: ['autorizado', 'falhou', 'cancelado'],
  autorizado: ['capturado', 'cancelado'],
  capturado: ['estornado'],
};

app.post('/webhooks/psp', async (req, res) => {
  if (!verifySignature(req.rawBody, req.header('x-signature'), process.env.PSP_SECRET!)) return res.sendStatus(400);
  const ev = JSON.parse(req.rawBody);
  await db.tx(async (t) => {
    // Restrição única em evento_id: a repetição do mesmo webhook não faz nada.
    const novo = await t.query('INSERT INTO eventos_psp (evento_id) VALUES ($1) ON CONFLICT DO NOTHING', [ev.id]);
    if (novo.rowCount === 0) return;
    const { rows } = await t.query('SELECT estado FROM pagamentos WHERE id = $1 FOR UPDATE', [ev.paymentId]);
    if (!NEXT[rows[0].estado]?.includes(ev.status)) return; // fora de ordem ou inválido: ignora (e registra)
    await t.query('UPDATE pagamentos SET estado = $1 WHERE id = $2', [ev.status, ev.paymentId]);
    if (ev.status === 'capturado') await lancar(t, { debito: 'psp_a_receber', credito: 'vendas', centavos: ev.amount });
  });
  res.sendStatus(200); // 200 rápido: o PSP reenvia quando não recebe
});

Três defesas juntas: assinatura (só o PSP chama), tabela de eventos com restrição única (duplicatas não fazem nada) e lista de transições permitidas (eventos fora de ordem não estragam o estado).

Em resumo

Vantagens e custos

Vantagens
  • Cobranças duplicadas ficam impossíveis por construção.
  • Qualquer saldo é explicável lançamento por lançamento.
  • Falhas de rede não deixam pagamento em estado desconhecido.
Custos e riscos
  • Mais tabelas e processos (webhooks, consulta, conciliação).
  • Estados assíncronos complicam a interface (“aguardando pagamento”).
  • Exige disciplina: nada de UPDATE em saldo.

Perguntar à IA

Monte um prompt pronto sobre Estudo de caso: um sistema de pagamentos na sua linguagem e no seu caso, e abra num chat gratuito.

ChatGPT Claude Perplexity

O prompt vai para o site que você escolher. Não cole senhas, tokens nem dados reais de clientes. Para Gemini e outros, copie e cole.

Referências

Continue aprendendo

Todos os temas de arquitetura de software →