1つのインターフェース、4つの支払い方法
3DS付きカード、ウォレット、コンビニ払い、口座振替を単一の内部サービスの背後に——決済ゲートウェイの抽象化が、プロバイダーの癖を注文フローから締め出す方法。
最初の決済手段の統合は、どこに置いても簡単です。後悔を教えてくれるのは3つ目です。その頃にはプロバイダーのコールバック語彙、リトライのタイミング、エラーの分類学が注文サービスに染み込んでいて、新しい手段が増えるたびに混沌が掛け算されます。私が関わったECプラットフォーム——GoのマイクロサービスをCloud Runで動かし、複数の下流サービスをまたいで購入をオーケストレーションする構成——では4つの手段をサポートしていました:3DS付きクレジットカード、ウォレット、コンビニ払い、口座振替。それを管理可能に保ったのは、早い段階のひとつの決定です:プロバイダーの語彙は決済ドメインで止まる。 専用のゲートウェイサービスが翻訳を所有し、コマースのサービス群——注文、ID、コミュニティ——はプロバイダーのペイロードを一度も見ませんでした。
境界の形
ゲートウェイサービスは2つの翻訳を所有します:
- アウトバウンド:内部の決済インテント(
authorize、capture、refund、手段固有のパラメータ)がプロバイダーのAPI呼び出しになる。 - インバウンド:プロバイダーのコールバック——成功、失敗、保留、そしてプロバイダー固有のあらゆる中間状態——が、他のすべてのサービスがすでに消費しているのと同じクリーンな内部イベントになる。
決済ドメインの外側では、すべてが1つの言語を話します。注文サービスは決済がオーソリされたことを知っていますが、3DSチャレンジのリダイレクトが何か、コンビニ払いの払込票番号がどんな見た目か、口座振替がカードと違うカレンダーで決済されることは知りません。
// プラットフォームの残りが見るもの——1つの語彙、4つの手段。
type PaymentEvent struct {
PaymentID string
OrderID string
Method Method // CARD | WALLET | CVS | DEBIT
Status Status // AUTHORIZED | CAPTURED | FAILED | EXPIRED | REFUNDED
OccurredAt time.Time
}
4つの手段は本当に違う
この抽象化が元を取るのは、4つの手段が1つのフローの変奏ではないからです。これらは4つの異なるステートマシンです:
- カード+3DS はほぼ同期ですが、途中にブラウザのリダイレクトを挟みます。ユーザーはチャレンジを放棄でき、それはタイムアウトが宣告するまで成功でも失敗でもありません。
- ウォレット は制御を別アプリに渡してコールバックで戻ります。レイテンシは普通は数秒ですが、ユーザーがすでに離脱した後にコールバックが届くこともあります。
- コンビニ払い は極端なケースです:「支払い」は約束にすぎません。ユーザーは払込票番号を受け取り、レジで払う——数時間後か、数日後か、あるいは永遠に払わない。注文フローは有効期限付きの保留状態に駐める必要があります。
- 口座振替 は銀行のタイムラインで決済され、他のすべてが成功した後に失敗しえます。
これらすべてを注文サービスの中でモデリングしていたら、注文のステートマシンが他の4つのステートマシンをimportすることになったでしょう。代わりにゲートウェイがそれぞれを共有語彙に圧縮します:すべては最終的に CAPTURED か FAILED か EXPIRED になり、「最終的に」が手段ごとにどれだけ長いかを知っているのはゲートウェイだけです。
難所はコールバック
プロバイダーのコールバックはプロバイダーの意味論でやって来ます:ときに重複し、ときに順不同で、常にプロバイダーの都合のタイミングで。被害を防いだ3つのルール:
- コールバックは信頼できない入力として扱う。 検証し、パースし、直ちに内部イベントへ写像する——生のペイロードはゲートウェイで止まり、監査のために保存されるだけで、先へは渡されません。
- すべてのコールバックハンドラーは冪等で、プロバイダーのトランザクションIDをキーにします。重複通知はインシデントではなく、普通の火曜日です。
- 内部イベントはTransactional Outboxを通って出ていく。 コールバックが決済状態を更新するのと、それを告げるイベントは原子的にコミットされます——注文フローを守るのと同じパターンが決済フローも守るのです。
ルール3の代替案——コールバックハンドラーから直接パブリッシュする——は、決済の衣装を着た二重書き込みバグそのものです。そして決済レコードは、静かなドリフトが最も許されない場所です。
これを作る人に伝えたいこと
- 境界は手段1つ目から入れる。 儀式に感じられてもです。ゲートウェイはカードだけを包んでいた頃は小さく、3つの手段が加わっても概念としては小さいまま——どの手段も決済ドメインにしか触れなかったからです。
- 内部語彙はプロバイダーのAPIではなく、自分の注文フローを中心に設計する。 プロバイダーのフィールドがプラットフォームの挙動を変えないなら、内部イベントに居場所はありません。
- 保留には締め切りを与える。 コンビニ払いのフローが教えてくれたのは、人間の行動を待つ状態には必ず有効期限と定期的な掃き掃除が要るということ。「待機」は管理する状態であって、はまり込む状態ではありません。
- 生のペイロードは保管する。 数週間後に精算の質問が届いたとき、答えるのは保存されたプロバイダーのペイロードです。正規化されたイベントは、そのために作られたものではありません。
- 境界は反論されることを見込んでおく。 納期のプレッシャーの下では、いずれ第二のサービスが自前のプロバイダークライアントを生やします——コピーされた認証情報の一つひとつが、抽象化がもう守っていない場所です。境界はレビューが強制する限りにおいてのみ保たれます。それは境界を書き残す理由であって、省く理由ではありません。
決済プロバイダーの統合は、何年も付き合う依存です。抽象化はプロバイダーを単純にはしません——その複雑さを知らなければならないサービスを、1つに保つのです。