Journal
Teknik3 min. læsning

Hændelsesdrevet fra bunden: sådan bliver én betaling til loyalitet, fastholdelse og en kvittering

En bookingplatform lever og dør på konsistens. Her er, hvordan en enkelt Stripe-betaling breder sig ud i loyalitetspoint, fastholdelse, en kvittering og en postering, hvorfor hver effekt er betinget af en rigtig tilstandsændring, og hvor vi er på vej hen med arkitekturen.

BUBookatus udviklingsteam

Når en kunde betaler for en booking, et depositum, en pakke eller et medlemskab, bør den ene hændelse sætte hele systemet i bevægelse. Hendes loyalitetssaldo bør vokse. Hun bør tælle som aktiv igen og ikke som frafalden. Hun bør få en kvittering. Og pengene bør lande i regnskabet til afstemning. Rammer man én af dem ved siden af, mister produktet stille og roligt tillid: et medlem bliver trukket, men hendes klip dukker aldrig op, eller et gensendt kald fra Stripe tæller et salg med to gange.

I en periode var hver af dem sin egen ø. Webhooken markerede tingene som betalt og stoppede der. Så gjorde vi betalingen til en hændelse og lod resten af systemet reagere på den.

Mønsteret: bred det ud, men kun ved en rigtig tilstandsændring

Diagram: en Stripe-betaling rammer en idempotent webhook, som kalder applyPaidRipple og breder sig ud til loyalitet, fastholdelse, kvittering og bogføring.
Én betaling, fire effekter. Webhooken er sømmen, og ringene i vandet gøres efter bedste evne og blokerer aldrig pengenes vej.

Stripe sender checkout.session.completed til én webhook. Håndteringen laver først den skrivning, der er kritisk for pengene (markér depositummet som betalt, sæt fakturaen til betalt, tildel klippene i pakken), og kalder derefter én fælles funktion, applyPaidRipple, for engagementsdelen. Ringene i vandet gøres efter bedste evne: hvert skridt er pakket ind, så en fejl bliver logget og slugt, for en ustabil mailudbyder må aldrig forhindre os i at registrere en rigtig betaling.

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.

Idempotens er hele sagen

Stripe leverer den samme hændelse mere end én gang. Det er en funktion og ikke en fejl, og det betyder, at enhver håndtering skal kunne køres to gange uden skade. To lag beskytter os. For det første bliver hændelsens id skrevet i en tabel over behandlede hændelser med en unik betingelse, så en gensendelse af præcis den samme hændelse bliver afvist med det samme. For det andet, og vigtigere, betinger hver vej sine ringe i vandet af sin egen ægte tilstandsændring: en boolsk værdi, der kun er sand den første gang rækken faktisk vender.

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.

Nøglen til at fjerne dubletter er ikke en luksus. Det er forskellen på, at et medlem får tak én gang, og at hun bliver trukket to gange.

Hvor det er på vej hen

I dag er ringene i vandet en udrulning i samme proces, og det er lige præcis den mængde maskineri, der passer til, hvor vi er: det er enkelt, transaktionelt og let at gennemskue. Men webhooken er bevidst en søm, og køreplanen gør den søm til en holdbar hændelsesbus. Formen bliver den samme, leveringen bliver stærkere.

  • En kø foran hver effekt (loyalitet, fastholdelse, beskeder), så en langsom modtager aldrig holder webhooken op, og gentagne forsøg sker automatisk med voksende ventetid.
  • En outbox-tabel, der skrives i samme transaktion som pengene og derefter sendes videre til bussen, så en effekt aldrig går tabt mellem 'betalt' og 'udgivet'.
  • At dele de tungeste modtagere (notifikationer, scoring af fastholdelse) op i deres egne services, efterhånden som belastningen vokser, mens den centrale bookingskrivning bliver ved med at være én stærkt konsistent enhed.
  • Et administreret lag til hændelser og køer i vores cloud, så skalering bliver en ændring i konfigurationen frem for en migrering.

Den lektie, der holder hele vejen igennem: hold pengenes vej stærkt konsistent og synkron, og gør alt det, der hænger på den, hændelsesdrevet, idempotent og efter bedste evne. En betaling er et faktum. Alt det gode, der følger af den, er en reaktion, og reaktioner skal kunne afspilles igen uden skade.

udviklinghændelsesdrevetarkitekturstripeidempotenskøreplan
Klar til at bruge det i praksis?

Bookatu giver dig en bookingside i dit eget look, depositum, medlemskaber, gavekort og påmindelser, med 0% kommission på dine bookinger.

Start gratis