日経COMPASSの技術構成・設計方針
日本経済新聞社は、実はBtoB 向けに企業や記事の情報を売るサービスをたくさん展開しております。本日はそんなサービスの1つである、日経COMPASSの事例紹介をしていきます。
https://www.nikkei.com/が 日経電子版で、その配下のcompassで展開されているサービスになり、記事や企業情報を検索して読めます。
次に構成技術、採用技術についてです。
フロントエンドはNext.jsを使っていて、最近は、App Routerとかも出てきていますが、Pages Routerの方で構築されています。バックエンドには、GraphQLサーバーのApolloサーバーを置いて、画面との通信はGraphQLを使っています。
画面に表示する情報は、日経の内外含めてほぼCOMPASS以外のAPIから取得しています。 記事や企業情報はもちろん、商品マスタや決済履歴についても、StripeのAPIを利用します。
アプリケーションが受け取った外部のAPIレスポンスをアプリケーションキャッシュとしてRedisに保存させています。画面の方は決済のためにStripeとやりとりしていて、 バックエンドではWebhookで完了通知を受け取っています。基本的に表示する情報は他のAPIに依存するような形です。
一応COMPASS内にもDBはありますが、セッション程度の情報しか持たないように意図的にしています。 決済履歴や商品マスタなどは自前のDBに置くサービスがほとんどですが、COMPASSではStripeのAPI越しに取得します。こうすることで、他のAPIと同じように扱えるのと同時に、提供する情報に一貫性を持たせています。
外部決済サービス「Stripe」を活用した決済フローの全体像
決済フローの全体像はこんな感じです。
まずは、ユーザーがコンテンツの購入ページを開くと決済モーダルが表示されます。ここで、COMPASSのバックエンドにPaymentIntent(Stripeにおける支払いを表すオブジェクト)を作成していて、支払いに関係するものは基本的にPaymentIntentに紐づくようにしています。
COMPASSでは、支払い対象のコンテンツの記事タイトルやコンテンツIDなどの情報をメタデータとしてPaymentIntentに持たせています。 ここではまだ決済が実行されていないので、Stripeのダッシュボードで見ると未完了として記録されています。
そして画面の方で、PaymentIntentの内容を基にStripe側で直接決済が実行されます。決済が完了すると、コールバックがStripeから返ってきて、決済完了モーダルが表示されるようになっています。
この後ユーザーはコンテンツ表示ページに遷移しますが、ここでCOMPASSのバックエンドは一度StripeにPaymentIntentの状態を問い合わせます。 決済完了であると判定できれば、コンテンツを返します。
以上がCOMPASSの決済の全体像です。
COMPASS上では購入履歴データを持っていません。Stripe APIで決済完了したPaymentIntentを検索することで、そのまま決済履歴として使えるようにしています。
支払い履歴にあるコンテンツ名や書誌情報などは決済履歴として表示しないといけない情報です。なので、あらかじめPaymentIntentのメタデータとして保持しておいて、支払い後にコンテンツが参照不可・期限切れとなった場合、コンテンツ自体は見れなくなるものの決済情報自体は消えない状態で購入履歴は表示させ続けることが可能です。
外部決済サービスによる受け入れテストにおける問題点
しかし、実際に受入テストをしていると問題が報告されました。
ユーザーが決済完了してからコンテンツの閲覧ページに自動遷移までに30秒程度かかっている、という状況でした。
決済完了画面のモーダル表示について、Stripeから決済完了のコールバックが来て表示されているので問題ありません。そのため、サーバー側の購入判定が遅いのでは、という感じになりました。
COMPASSでは、買ってから半年以上経つと閲覧不可になるというコンテンツの閲覧期限があります。この閲覧期限がある関係上、決済時刻が重要になってきます。StripeではChargeというオブジェクトが司っていて、支払いの度に新しいChargeが作成されるような形になっています。
通常PaymentIntentにChargeは1つだけ紐づくので、バックエンドではPaymentIntentにChargeが紐づいたことで決済完了と判定していました。 画面の決済完了コールバックはすぐ返ってきましたが、PaymentIntentにChargeが紐づくまで30秒ほどかかり、決済完了判定にも時間がかかっていました。
この紐付けにかかる時間をずっと待っていたので、決済完了判定にも時間がかかってしまっていました。
PaymentIntentt< >Chargeの紐付き(決済完了)判定の高速化に向けた取り組み
この判定をどう高速化したかについて紹介します。このシーケンス図については決済周りだけを抜粋したものです。
まずは、画面のコールバックとほぼ同時に、決済完了Webhookも呼ばれていることが分かったので、Webhookで届いたPaymentIntentをRedisに60秒程度の短命で保存するようにしました。
実際に決済判定を行う際には、Stripeの履歴を確認すると同時にRedisにも保存されたPaymentIntentが無いかを調べて、 StripeまたはRedisからPaymentIntentが取得できた場合は決済完了とする形になっています。
こうすることで今まで30秒ほどかかっていた処理が1秒未満に短縮され、ユーザーも支払い完了後にすぐにコンテンツを閲覧できるようになりました。
決済完了後にPaymentIntentにChargeが紐づくのにはある程度の時間がかかるため、決済完了のWebhookで受け取ったPaymentIntentを一時的に保存して使うことで、決済判定を高速化したという形になります。