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.
Arraste para o lado para ver o diagrama inteiro.
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
// 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
- Chave de idempotência com restrição única.
- Máquina de estados com transições válidas.
- Webhooks: assinatura, deduplicação e ordem.
- Razão imutável em partidas dobradas, em centavos.
- Conciliação diária com o PSP.
Vantagens e custos
- 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.
- 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.
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.