Verhalen
Techniek3 min lezen

Event-driven van opzet: hoe één betaling loyaliteit, klantbehoud en een bon wordt

Een boekingsplatform staat of valt met consistentie. Zo waaiert één Stripe-betaling uit naar loyaliteitspunten, klantbehoud, een bon en een boekingsregel, waarom elk effect achter een echte statuswijziging zit, en waar we met de architectuur naartoe gaan.

HTHet technische team van Bookatu

Als een klant betaalt voor een boeking, een aanbetaling, een pakket of een lidmaatschap, hoort die ene gebeurtenis het hele systeem in beweging te zetten. Zijn loyaliteitssaldo hoort te groeien. Hij hoort weer als actief te tellen, niet als weggezakt. Hij hoort een bon te krijgen. En het geld hoort in de boeken te landen om af te stemmen. Gaat er één van die dingen mis, dan verliest het product stilletjes vertrouwen: een lid wordt afgeschreven maar zijn tegoed verschijnt nooit, of een nieuwe poging van Stripe telt een verkoop dubbel.

Een tijdlang was elk van die dingen een eigen eiland. De webhook zette dingen op betaald en stopte daar. Dus hebben we van de betaling een gebeurtenis gemaakt, en de rest van het systeem erop laten reageren.

Het patroon: uitwaaieren, maar alleen bij een echte statuswijziging

Diagram: een Stripe-betaling komt binnen bij een idempotente webhook, die applyPaidRipple aanroept en uitwaaiert naar loyaliteit, klantbehoud, bon en boekhouding.
Eén betaling, vier effecten. De webhook is de naad; de rimpeling is best-effort en blokkeert het geldpad nooit.

Stripe stuurt checkout.session.completed naar één webhook. De handler doet eerst de schrijfactie die met geld te maken heeft (de aanbetaling op betaald zetten, de factuur omzetten naar betaald, het pakkettegoed toekennen) en roept daarna één gedeelde functie aan, applyPaidRipple, voor de betrokkenheidskant. De rimpeling is best-effort: elke stap zit ingepakt zodat een fout wordt gelogd en opgeslokt, want een haperende mailprovider mag ons nooit tegenhouden om een echte betaling vast te leggen.

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.

Idempotentie is waar alles om draait

Stripe levert dezelfde gebeurtenis meer dan eens af. Dat is een functie en geen bug, en het betekent dat elke handler het veilig twee keer moet kunnen doen. Twee lagen beschermen ons. Ten eerste wordt het id van de gebeurtenis vastgelegd in een tabel met verwerkte gebeurtenissen met een unieke sleutel, zodat een herlevering van precies dezelfde gebeurtenis meteen afslaat. Ten tweede, en belangrijker, zet elk pad zijn rimpeling achter een eigen echte statuswijziging: een booleaanse waarde die alleen de eerste keer waar is dat de rij daadwerkelijk omslaat.

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.

De ontdubbelsleutel is geen leuke extra. Hij is het verschil tussen een lid dat één keer bedankt wordt en een lid dat twee keer wordt afgeschreven.

Waar dit naartoe gaat

Vandaag is de rimpeling een uitwaaiering binnen het proces, en dat is precies de juiste hoeveelheid machinerie voor waar we nu staan: simpel, transactioneel en makkelijk te doorgronden. Maar de webhook is met opzet een naad, en op de roadmap wordt die naad een duurzame event bus. De vorm blijft hetzelfde; de aflevering wordt sterker.

  • Een wachtrij voor elk effect (loyaliteit, klantbehoud, berichten), zodat een trage afnemer de webhook nooit ophoudt en nieuwe pogingen automatisch met uitstel gebeuren.
  • Een outbox-tabel die in dezelfde transactie als het geld wordt geschreven en daarna wordt doorgegeven aan de bus, zodat een effect nooit verloren gaat tussen 'betaald' en 'gepubliceerd'.
  • De zwaarste afnemers (meldingen, scores voor klantbehoud) opsplitsen in eigen uitrolbare services naarmate de belasting groeit, terwijl de kern van de boekingsschrijfactie één sterk consistente eenheid blijft.
  • Een beheerde laag voor gebeurtenissen en wachtrijen in onze cloud, zodat schaal een instelling wordt in plaats van een verhuizing.

De les die overal standhoudt: houd het geldpad sterk consistent en synchroon, en maak alles wat eraan hangt event-driven, idempotent en best-effort. Een betaling is een feit. Alles goeds dat eruit volgt is een reactie, en reacties moeten veilig opnieuw af te spelen zijn.

engineeringevent drivenarchitectuurstripeidempotentieroadmap
Klaar om hiermee aan de slag te gaan?

Bookatu geeft je een boekingspagina in je eigen huisstijl, met aanbetalingen, lidmaatschappen, cadeaubonnen en herinneringen, en 0% commissie op je boekingen.

Gratis beginnen