ジャーナル
エンジニアリング1分で読めます

設計としてのイベント駆動。1件の支払いが、ポイントと再来店と領収書になるまで

予約プラットフォームの生死は一貫性にかかっています。Stripe の1件の支払いが、ポイント、再来店の判定、領収書、台帳へと広がる仕組み、あらゆる波及が本物の状態変化に紐づいている理由、そしてこの構成をこれからどこへ持っていくかをご説明します。

BエBookatu エンジニアリングチーム

お客様が予約、前払い、回数券、会員費に支払うとき、その一つの出来事がシステム全体を動かすべきです。ポイントの残高が増え、離脱ではなく再び利用中として数えられ、領収書が届き、そして金額が突き合わせのために帳簿に入る。このどれか一つでも間違えば、製品は静かに信頼を失います。会員に請求したのに回数が付かない、Stripe の再送で売上が二重に数えられる、といった形でです。

しばらくのあいだ、これらはそれぞれ孤島でした。webhook は支払い済みの印をつけて、そこで終わっていたのです。そこで支払いをイベントにして、残りのシステムがそれに反応するようにしました。

型。分岐させる。ただし本物の状態変化のときだけ

図。Stripe の支払いが冪等な webhook に届き、applyPaidRipple を呼び、ポイント、再来店、領収書、台帳へと分岐する
1件の支払いから4つの処理へ。webhook が継ぎ目で、波及はベストエフォートであり、お金の経路を止めることはありません。

Stripe は checkout.session.completed を1つの webhook に送ります。ハンドラーはまずお金にとって重要な書き込みを行い(前払いを支払い済みにする、請求書を支払い済みに切り替える、回数券の残数を付与する)、そのあとで関与の側のために共通の関数 applyPaidRipple を1回だけ呼びます。波及はベストエフォートです。各手順は包まれていて、失敗しても記録して握りつぶします。メールの配信事業者が不安定だからといって、本物の支払いの記録が止まってはならないからです。

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.

冪等性がすべてです

Stripe は同じイベントを複数回届けます。これは不具合ではなく仕様で、つまりどのハンドラーも2回実行されて安全でなければなりません。守りは2層です。1つ目は、イベントIDを一意制約つきの処理済みイベントの表に記録すること。まったく同じイベントの再送はそこで打ち切られます。2つ目、そしてより重要なのは、各経路が自分自身の本物の状態変化に波及を紐づけていることです。行が実際に切り替わった最初の一度だけ真になる真偽値を使います。

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.

重複排除の鍵は、あれば良いものではありません。会員が一度お礼を言われるか、二重に請求されるかの分かれ目です。

これからの方向

今のところ波及はプロセス内の分岐で、現在地に対してはちょうど良い量の仕掛けです。単純で、トランザクションの中にあり、筋道を追いやすい。ただし webhook はあえて継ぎ目にしてあり、ロードマップではその継ぎ目を耐久性のあるイベントバスにします。形はそのままで、配送だけが強くなります。

  • 各処理(ポイント、再来店、通知)の前にキューを置きます。遅い受け手が webhook を止めることはなくなり、再試行は待ち時間を伸ばしながら自動で行われます。
  • お金と同じトランザクションで書き込むアウトボックスの表を用意し、そこからバスへ中継します。支払い済みと公開のあいだで処理が失われることがなくなります。
  • 負荷が増えたら、一番重い受け手(通知、再来店のスコアリング)を独立した配備単位に切り出します。中心となる予約の書き込みは、強い一貫性を持つ一つの単位のままにします。
  • クラウド側の管理されたイベントとキューの層に載せます。規模の拡大が、移行ではなく設定変更で済むようになります。

全体を通して変わらない教訓はこれです。お金の経路は強い一貫性を持たせて同期的に保ち、そこにぶら下がるものはすべてイベント駆動、冪等、ベストエフォートにする。支払いは事実です。そこから続く良いことはすべて反応であり、反応は何度でも安全に再生できるべきです。

エンジニアリングイベント駆動アーキテクチャstripe冪等性ロードマップ
さっそく試してみませんか。

Bookatuなら、あなたのブランドの予約ページ、デポジット、会員制度、ギフトカード、リマインダーが揃って、予約手数料は0%です。

無料で始める