9:00 にセールが始まります。CDN が商品ページを支えていても、チェックアウトが失敗します。2 人の購入者が同じジャケットの最後の 1 点を買い、タイムアウト後のリトライで 1 枚のカードに決済の保留が 2 件残り、イベントが失われたせいで倉庫が注文を 1 時間遅れで知ることになります。
どれも容量の問題ではありません。境界の問題です。どのデータを誰が所有するのか、そして購入者が待っている間にどのステップを完了させる必要があるのか、ということです。
この記事では、中規模ストア向けの現実的な EC アーキテクチャを順に見ていきます。コンポーネントとデータ所有、チェックアウト経路のステップごとの流れ、その後の注文パイプライン、ピーク時のトラフィック、そしてプラットフォームの図です。
購入者の行動から始める
5 つのジャーニーでストアの動作のほとんどをカバーでき、それぞれ要求は大きく異なります。
| ジャーニー | 経路上のコンポーネント | 古くてもよいもの |
|---|---|---|
| 閲覧 | CDN、ゲートウェイ、Catalog | キャッシュされたページの価格と在庫(数分) |
| 検索 | 検索インデックス | 変更された商品(数秒) |
| カート | Cart | 購入者がたった今変更した内容は不可 |
| チェックアウト | Checkout、Inventory、決済プロバイダ、Orders | 不可 |
| 注文追跡 | Orders、Fulfillment | 配送ステータス(数分) |
閲覧と検索はリクエストの大半を担い、古いデータを許容します。チェックアウトは少数のリクエストを担い、古いデータを一切許容しません。アーキテクチャの大部分は、この分け方から導かれます。
コンポーネントと、それぞれが所有するデータ
エッジでは CDN がページと画像をキャッシュし、API ゲートウェイ(またはクライアント種別ごとの backend for frontend)が認証、レート制限、残りのリクエストの振り分けを行います。その背後の各サービスは自分のデータを所有します。
| サービス | 所有するもの | 一般的なストア | 整合性の要件 |
|---|---|---|---|
| Catalog | 商品、バリエーション、価格 | Postgres、キャッシュ経由で読み取り | 結果整合。チェックアウトが価格を再取得 |
| Search | ファセット付き商品インデックス | OpenSearch または Elasticsearch | 結果整合。Catalog の変更から更新 |
| Cart | 購入者ごとのカート明細 | TTL 付き Redis | 購入者ごとの read-your-writes |
| Checkout | セッション:合計、引当 ID、決済 ID | Postgres | セッション内で強い整合性 |
| Inventory | SKU ごとの在庫、引当 | Postgres | 強い整合性:売り越しは不可 |
| Orders | 注文、状態履歴、アウトボックス | Postgres | 強い整合性:注文ごとに書き込み側は 1 つ |
| 決済サービスプロバイダ(PSP) | オーソリ、キャプチャ、返金 | PSP | PSP の記録が正 |
| Fulfillment | 出荷、追跡番号 | WMS または 3PL | 結果整合。イベントで駆動 |
他のサービスは所有者の API またはイベントを使い、テーブルには触れません。在庫と注文にはトランザクションが必要です。カートと検索インデックスは、遅れても、書き込みが 1 件失われても、実害はありません。
チェックアウト経路をステップごとに見る
チェックアウトは、購入者が複数のサービスを同時に待つ唯一のフローです。
カートの価格を再計算する。 Checkout はカートを読み取り、Catalog から現在の価格を取得して、税、送料、割引をサーバー側で計算します。ブラウザの合計を信用してはいけません。
有効期限付きで在庫を引き当てる。 ここでは 15 分です。
-- One transaction: take the stock, then record the hold UPDATE stock SET reserved = reserved + 2 WHERE sku = 'JKT-042-M' AND on_hand - reserved >= 2; -- 0 rows: out of stock, stop INSERT INTO reservations (checkout_id, sku, qty, expires_at) VALUES ('chk_41a', 'JKT-042-M', 2, now() + interval '15 minutes');2 人の購入者が最後のジャケットを争うと、PostgreSQL のデフォルトの分離レベル Read Committed では 2 つ目の更新が待機し、新しい行に対して WHERE 句を再評価するため、どちらか一方は 0 行になります。スイーパーが期限切れの引当を在庫に戻します。ホスト型の決済ページを使う場合は、時間を揃えてください。Stripe Checkout のセッションは、より短い有効期限(最短 30 分)を設定するか自分で失効させない限り 24 時間続きます。
checkout.session.expiredが、商品を解放するタイミングを知らせます。冪等性キーを付けて決済を作成する。 Checkout は PSP に、サーバー側で計算した合計の決済を手動キャプチャで依頼し、冪等性キーは呼び出しの前に保存します。タイムアウト後は同じキーでリトライします。Stripe は保存済みのレスポンスを返し、2 件目の決済は作成しません。キーは最大 255 文字で、24 時間後に削除されることがあります。
オーソリする。 決済の途中で期限切れにならないよう、先に在庫の引当を延長します。購入者は PSP の入力欄にカード情報を入力するため、カード番号がサーバーに届くことはなく、カード発行会社が 3-D Secure を求める場合もあります。成功すると資金が保留されます。手動キャプチャの Stripe の決済は
requires_captureに移り、payment_intent.amount_capturable_updatedを発行します。ブラウザのリダイレクトではなく、この webhook を信頼してください。決済ゲートウェイ連携図も参照してください。注文を確定する。 Orders は注文を
placedで作成し、同じトランザクションでアウトボックステーブルにOrderPlacedを書き込みます。その後 Checkout が Inventory に、引当を割り当てへ変えるよう指示します。出荷時にキャプチャする。 Fulfillment が出荷すると、Orders がキャプチャします。執筆時点(2026 年 11 月)で、Stripe のドキュメントによると、オンラインのカードオーソリは通常 7 日、加盟店起点の Visa 取引では 5 日(正確には 4 日と 18 時間)有効です。その後は資金が解放され、決済はキャンセルされます。ほとんどのカード決済でキャプチャは 1 回だけ可能で、部分キャプチャを行うと残りは解放されます。
注文が存在する前にステップが失敗した場合は、前のステップを逆順に取り消します。オーソリをキャンセルし、引当を解放します。これは、Checkout をオーケストレーターとするサガです。
金額、在庫、注文状態が変わるタイミング
| 注文状態 | 設定されるタイミング | 決済 | 在庫 |
|---|---|---|---|
| Checkout 中 | 在庫が引き当てられたとき | 作成済み、未オーソリ | 15 分間保留 |
| Placed | PSP がオーソリを通知したとき | 資金を保留 | 注文に割り当て済み |
| Shipped | Fulfillment が出荷したとき | キャプチャ済み | 手持ち在庫から減算 |
| Canceled | 出荷前にキャンセルされたとき | オーソリをキャンセル | 解放 |
| Returned | 返品を受領したとき | 返金済み | 検品後に在庫へ戻す |
難しいケースは行と行の間にあります。引当が失効した後にオーソリが届いた場合、在庫が残っていれば再度引き当て、なければオーソリをキャンセルして購入者に伝えます。取り寄せ(バックオーダー)が 7 日の期間を超えるなら、最初の出荷で全額をキャプチャするか、PSP が提供していれば延長オーソリを使うか(Stripe によると一部のカードネットワークで最大 30 日)、先に請求して出荷できない分を返金します。ACH や iDEAL のように、キャプチャを別に行えない決済手段もあります。
注文が確定した後
placed 以降はすべて非同期で動きます。リレーが OrderPlaced をアウトボックスから order-events トピックに発行するため、コミットとブローカーの間でイベントが失われることはありません(トランザクショナルアウトボックスパターンを参照)。Fulfillment はピッキング、梱包、出荷を行い、その後 OrderShipped を発行してキャプチャを起動します。Notifications はメールを送り、Analytics はすべてのイベントをウェアハウスに取り込みます。
配信は at-least-once なので、コンシューマーはイベント ID で重複を排除します。トピックのキーを注文 ID にすると、各注文のイベントの順序が保たれます。
マイクロサービスアーキテクチャ図では、代わりに OrderPlaced の後でカードに請求します。これは購入者の操作が不要な決済に向いています。3-D Secure には購入者が居合わせる必要があるため、この設計ではチェックアウト内でオーソリします。
ピークトラフィックを乗り切る
セールの 1 時間に 7,200 件の注文、コンバージョン率 2%、1 セッションあたり 10 ページビューと仮定します。すると 360,000 セッション、360 万ページビューとなり、1 秒あたり約 1,000 ページビューに対して注文は 1 秒あたり 2 件です。それぞれに別々の対策が必要です。
- カタログをエッジでキャッシュする。
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=60を使うと、CDN は商品ページを 5 分間配信し、その後はバックグラウンドで再取得する間さらに最大 1 分配信します。これは RFC 5861 の定義どおりです。ヒット率 90% なら、オリジンへのページビューは 1,000 ではなく 1 秒あたり 100 になります。価格変更時はパージしてください。チェックアウトはどのみち再確認します。 - チェックアウトの前に待機列を置く。 仮想待機室は、Inventory と PSP が処理できる速度で購入者を入場させます。チケット予約システムの設計と同じ考え方です。
- ホット SKU を守る。 1 つの SKU への引当は 1 つの行を 1 件ずつ更新します。トランザクションあたりのロック時間が 2 ms なら、その行は 1 秒あたり約 500 件の引当が上限です。トランザクションを短く保って PSP 呼び出しを含めず、目玉商品の在庫を複数の行に分け、注文あたりの数量に上限を設けてください。
- PSP のレート制限を把握する。 セールの前に確認しておきます。
これより小さく始める
多くのストアは、これをすべて構築する必要はありません。Shopify や BigCommerce のようなホスト型プラットフォームなら、カタログ、カート、チェックアウトを代わりに運用してくれます。その場合、図に描くのはその周りのアプリと webhook です。
独自に作る場合は、モジュラーモノリスから始めるのが最適なことがよくあります。同じ境界を、1 つのデプロイ単位の中のモジュールとして持つ形です。2019 年に Shopify のエンジニアは、最大級の Ruby on Rails コードベースをマイクロサービスに分割せず、注文、配送、在庫、請求を軸に再編成したことを紹介しました。モジュールが独自のスケーリングやリリースを必要としたときにサービスとして切り出してください。図はそのまま使えます。
EC プラットフォームの図
ダイアグラムの内容
- Shopper
- CDN
- API gateway / BFF
- Catalog
- Search
- Cart
- Checkout
- Orders
- Inventory
- Payment provider (PSP)
- order-events
- Fulfillment
- Notifications
- Analytics warehouse
- HTTPS
- misses, API
- GET /products
- GET /search
- POST /cart/items
- POST /checkout
- GET /orders/{id}
- product changes
- read cart
- reserve, 15 min
- authorize + idempotency key
- place order
- capture on OrderShipped
- OrderPlaced
- placed, shipped
- all events
図の読み方は次のとおりです。
- リクエストは左上から入ります。 購入者、CDN、そして各ジャーニーを 1 つのサービスに振り分けるゲートウェイの順です。
- 青は閲覧経路です。 破線の辺は、検索インデックスが商品の変更に追いつく流れです。
- 琥珀色はチェックアウト経路で、購入者が待つ部分です。 カートの読み取り、在庫の引当、オーソリ、注文の確定を行います。
- 決済プロバイダは 2 回呼ばれます。 チェックアウト中のオーソリと、
OrderShipped時のキャプチャ(青緑の破線)です。 - 紫の破線の辺はイベントです。 アウトボックスから order-events を経て、Fulfillment、Notifications、Analytics に流れます。
- 各サービスは自分のストアを明記し、 Markdown ブロックに数値をまとめています。
EC アーキテクチャ図をわかりやすく描くには
- ゲートウェイの辺にルートを書く。
GET /productsのように書くと、読み手がページとサービスを対応づけられます。 - すべてのサービスにストア名を書く。 共有データベースは見逃せないようにします。
- 待つ呼び出しは実線、後で動くものは破線にする。 購入者が待つ呼び出しは、イベントと違って見えるべきです。
- 辺に時間を書く。 引当の保持時間とキャプチャの起動条件は、最もレビューが必要な部分です。
- オーソリとキャプチャは 2 本の辺で描く。 PSP への矢印が 1 本だと、重要なタイミングが隠れます。
- 省くもの: Pod、リージョン、税や不正検知のベンダー、バックオフィス。
Archboard で作る
- Google または GitHub でサインインし、ダイアグラムを作成します。
- C4 Model パックから人物を追加し、その下にクラウドパックから CDN と APIゲートウェイを追加します。
- C4 Model パックから コンテナ ノードを 5 つ、縦に並べて追加します。Catalog、Search、Cart、Checkout、Orders で、それぞれの説明にストアを書きます。
- Inventory 用のコンテナ、決済プロバイダ用の外部システム、order-events 用にデータパックからトピックを追加します。その右に、Fulfillment と Notifications 用のコンテナノードと、データパックからデータウェアハウスを追加します。
- ゲートウェイを 5 つのサービスにルートラベル付きで接続します。チェックアウトの呼び出しは実線に、商品の変更、キャプチャ、イベントは破線にします。
- 基本パックから数値を載せた Markdown ブロックを追加し、凡例と主要なルール用にテキストのメモを追加します。
サービスを Compose ファイルから動かしている場合は、Docker Compose からダイアグラムへツールがボックスの下書きを作ります。マイクロサービスアーキテクチャのテンプレートも出発点になります。この例で使うダイアグラムノードは 15 個で、Free プランの範囲に収まります。
よくある質問
EC アーキテクチャの主なコンポーネントは何ですか? CDN、API ゲートウェイ、カタログ、検索、カート、チェックアウト、在庫、注文の各サービス、決済プロバイダ、そして Fulfillment、Notifications、Analytics です。ホスト型プラットフォームは、その大半をまとめて提供します。
オンラインストアはマイクロサービスを使うべきですか? デフォルトでは不要です。同じ境界を持つモジュラーモノリスのほうが運用が簡単で、Shopify も中核にそれを選びました。モジュールが独立したスケーリングやリリースを必要としたときにサービスを切り出してください。
在庫の売り越しはどう防ぎますか? SKU ごとに、アトミックな条件付き更新で在庫を引き当て、各引当に有効期限を設け、注文が確定したときにだけ割り当てます。キャッシュされたページから在庫の有無を判断してはいけません。
オンラインストアはいつ決済をキャプチャすべきですか? 物理的な商品では、通常は出荷時です。出荷した分だけ請求できます。オンラインのカードオーソリは通常 7 日間有効です。Stripe では Klarna が 28 日、PayPal が 10 日で、Stripe はこれを 20 日に延長しようとします。
EC サイトはトラフィックの急増にどう対処しますか? カタログを CDN から配信し、チェックアウトの前で購入者を待機させ、ホット SKU と決済プロバイダを守ります。チェックアウトの容量は、ページビューとは別に計画してください。