Diário
Engenharia3 min de leitura

Orientado a eventos por desenho: como um pagamento se transforma em fidelização, retenção e recibo

Uma plataforma de marcações vive ou morre da sua consistência. Veja como um único pagamento Stripe se ramifica em pontos de fidelização, retenção, um recibo e um lançamento contabilístico, porque cada efeito depende de uma mudança de estado real, e para onde vamos levar a arquitetura a seguir.

AEA equipa de engenharia do Bookatu

Quando um cliente paga uma marcação, um sinal, um pacote ou uma subscrição, esse único acontecimento deve mover o sistema inteiro. O saldo de fidelização dele deve crescer. Deve voltar a contar como ativo, e não como perdido. Deve receber um recibo. E o dinheiro deve entrar nas contas para conciliação. Se uma destas coisas falha, o produto perde confiança em silêncio: um membro é cobrado mas os créditos nunca aparecem, ou uma repetição do Stripe conta a mesma venda duas vezes.

Durante algum tempo, cada uma destas peças era uma ilha. O webhook marcava as coisas como pagas e ficava por aí. Por isso transformámos o pagamento num evento e deixámos o resto do sistema reagir a ele.

O padrão: ramificar, mas só com uma mudança de estado real

Diagrama: um pagamento Stripe chega a um webhook idempotente, que chama applyPaidRipple, ramificando para fidelização, retenção, recibo e razão.
Um pagamento, quatro efeitos. O webhook é a costura; a propagação é best-effort e nunca bloqueia o caminho do dinheiro.

O Stripe envia checkout.session.completed para um webhook. O handler faz primeiro a escrita crítica do dinheiro (marcar o sinal como pago, passar a fatura a paga, atribuir o crédito do pacote) e depois chama uma única função partilhada, applyPaidRipple, para o lado do envolvimento. A propagação é best-effort: cada passo está envolvido de forma a que uma falha seja registada e engolida, porque um fornecedor de email instável nunca pode impedir-nos de registar um pagamento real.

ts
// In the webhook, after the money-critical write succeeds:if (fresh) {  await applyPaidRipple({    orgId,    customerId,    amountCents,    loyaltyReason: "Package purchase",    receiptTo: email,    receiptDedupeKey: `payment_receipt:package:${session.id}`,  });}// applyPaidRipple: loyalty -> lastVisitAt bump -> transactional receipt,// each in its own try/catch. The ledger write runs unconditionally below.

A idempotência é o jogo todo

O Stripe vai entregar o mesmo evento mais do que uma vez. Isso é uma funcionalidade e não um defeito, e significa que cada handler tem de ser seguro para correr duas vezes. Protegem-nos duas camadas. Primeiro, o id do evento é registado numa tabela de eventos processados com uma restrição de unicidade, por isso uma reentrega exatamente do mesmo evento é curto-circuitada. Segundo, e mais importante, cada caminho condiciona a sua propagação a uma mudança de estado genuína sua: um booleano que só é verdadeiro na primeira vez que a linha muda mesmo.

ts
// markInvoicePaid flips 'open' -> 'paid' and returns true ONLY on that// real transition. A redelivery sees 'paid' already and returns false.const flipped = await markInvoicePaid(invoiceId, paymentIntentId, amountTotal);if (flipped) {  await applyPaidRipple({ /* ...loyalty, retention, receipt */ });}// So even if the event-id dedupe is bypassed, the ripple still fires once.

A chave de deduplicação não é um extra simpático. É a diferença entre um membro ser agradecido uma vez e ser cobrado duas.

Para onde isto vai

Hoje a propagação é um fan-out dentro do processo, o que é a quantidade certa de maquinaria para onde estamos: é simples, transacional e fácil de raciocinar. Mas o webhook é de propósito uma costura, e o roadmap transforma essa costura num barramento de eventos durável. A forma mantém-se; a entrega fica mais forte.

  • Uma fila à frente de cada efeito (fidelização, retenção, mensagens), para que um consumidor lento nunca segure o webhook e as repetições sejam automáticas com backoff.
  • Uma tabela outbox escrita na mesma transação que o dinheiro e depois retransmitida para o barramento, para que um efeito nunca se perca entre o 'pago' e o 'publicado'.
  • Separar os consumidores mais pesados (notificações, cálculo de retenção) nos seus próprios serviços implantáveis à medida que a carga cresce, mantendo a escrita central da marcação como uma unidade única e fortemente consistente.
  • Uma camada gerida de eventos e filas na nossa cloud, para que escalar seja uma alteração de configuração e não uma migração.

A lição que se aplica a tudo isto: mantenha o caminho do dinheiro fortemente consistente e síncrono, e torne tudo o que pende dele orientado a eventos, idempotente e best-effort. Um pagamento é um facto. Tudo de bom que dele decorre é uma reação, e as reações devem ser seguras de repetir.

engenhariaorientado a eventosarquiteturastripeidempotênciaroadmap
Pronto para pôr isto em prática?

O Bookatu dá-lhe um site de marcações com a sua marca, sinais, subscrições, vales-presente e lembretes, com 0% de comissão nas suas marcações.

Começar grátis