Slide 1

Slide 1 text

Stailer におけるコトを残すデータ設計とイベ ント駆動アーキテクチャ 新しい機能では「" 状態" ではなく" 事実( イベント)" を記録する」ことを選びました 1

Slide 2

Slide 2 text

10x を創るという価値観で、小売業界のDX にチャレン ジしている会社 Stailer ネットスーパー向けシステム EC サイトから商品準備、配送までを一気通貫で扱 う 実際の店舗と共存するサービスモデル 10X とStailer ALL RIGHTS RESERVED BY 10X, INC. 2

Slide 3

Slide 3 text

在庫マスタの管理(パートナー企業) EC サイトの注文受付(お客様と店舗) 商品準備のオペレーション(店舗スタッフ) 配送オペレーション(配送スタッフ) 請求・売上管理(パートナー企業) ネットスーパーに関わる領域と人々 ALL RIGHTS RESERVED BY 10X, INC. 3

Slide 4

Slide 4 text

広い領域を扱う 大量の在庫データ取り込み EC サイト 商品準備(ピックパック) 配送オペレーション などなど ネットスーパー向けシステム ALL RIGHTS RESERVED BY 10X, INC. 4

Slide 5

Slide 5 text

概要 お客様からの受注に基づいて商品を準備する工程 ピッキング: 商品を売り場から選ぶ パッキング: 商品を箱詰めする パッキングされた箱は後工程( 配送/ お渡し) へ引き渡される Stailer で扱うピックパック業務の一部 ALL RIGHTS RESERVED BY 10X, INC. 5

Slide 6

Slide 6 text

ピッキングの具体的なフロー 1. 商品が見つかれば バーコードをアプリでスキャン システムは見つけた数を記録(→ パッキング工程の対象へ) 2. 見つからなければ 「品切れ」としてアプリを通して記録 システムは注文取引へ「品切れ」として「お届けなし」を反映 Stailer で扱うピックパック業務の一部 ALL RIGHTS RESERVED BY 10X, INC. 6

Slide 7

Slide 7 text

クライアント・サーバ間通信の設計 店舗のネットワーク環境等を考慮したのか、一定時間の操作結果をクライアントに蓄積、 まとめてサーバに反映する方式 「事実」ではなく「状態(スナップショット) 」を送信 「りんごを1 つピックした、品切れが1 つ発生した」ではなく 「りんごは3 個ピック済み、品切れは1 個である」を送信 Before: 以前の設計 ALL RIGHTS RESERVED BY 10X, INC. 7

Slide 8

Slide 8 text

1. データ不整合の頻発 2. 行動分析の困難さ 3. 信頼性の欠如 4. 調査の困難さ 5. 機能開発の制約 ざくっと課題の根を整理すると データ設計: 事実が永続化されていない コミュニケーション設計: リアルタイム性が低く、整合性が保てない Before: 抱えていた課題 ALL RIGHTS RESERVED BY 10X, INC. 8

Slide 9

Slide 9 text

Order とは 「注文にまつわるすべての契約、業務を注文ID で一意にできることすべて詰 め込んだモノ」 注文確定からお渡し完了まで、あらゆる詳細を扱う Stailer のあらゆるアクター EC 、ピック、配送、請求... のスタッフが関心を持つ 抱えていた問題 巨大で複雑: あらゆる業務ロジックが混在し、相互に依存 変更の影響範囲: ピックパックの変更が、請求や配送に影響するリスク ステータスの爆発: 「未ピック」 「保留」 「品切れ」... 各領域で考慮が必要 巨大なOrder (注文)クラス ALL RIGHTS RESERVED BY 10X, INC. 9

Slide 10

Slide 10 text

1 ドキュメントの限界 Order はFirestore 上で1 つのドキュメントとして表現されていた ピッキング業務は「商品」ごとに並列で行われるが、更新先は単一のOrder 書き込み競合 が頻発 1 ドキュメントあたりの書き込み頻度制限(1 回/ 秒程度)に抵触 トランザクションスクリプトの弊害 Order のあらゆるフィールドが公開され、スクリプトが自由に書き換え 単体テストが困難 Firestore エミュレータ必須 セットアップが重い Order とFirestore 、業務事情の相性問題 ALL RIGHTS RESERVED BY 10X, INC. 10

Slide 11

Slide 11 text

トランザクションスクリプトの弊害 ALL RIGHTS RESERVED BY 10X, INC. 11

Slide 12

Slide 12 text

## トランザクションスクリプトの トランザクションスクリプトの弊害 ALL RIGHTS RESERVED BY 10X, INC. 12

Slide 13

Slide 13 text

トランザクションスクリプトの弊害( 地道な改善もしているんだよ) ALL RIGHTS RESERVED BY 10X, INC. 13

Slide 14

Slide 14 text

物理削除 or 論理削除? の議論なしに、しれっと物理削除が横行 失われる「事実」 しれっと物理削除が起きるのはなぜ? Order クラスが持っているList をfilter(where) で書き換えるような操作をなんとなく書く Order クラスをほぼそのままJSON に変換してFirestore に保存 filter で除外した要素がFirestore 上から物理削除される 横行するしれっと物理削除 ALL RIGHTS RESERVED BY 10X, INC. 14

Slide 15

Slide 15 text

きっかけ: シングルバスケットピックの登場 業務の根幹を支える機能の実現 新たなピックパック方式(シングルバスケットピック)の要件 既存システム(総量ピック)の使い回しが困難 「ここでフルスイングしないと一生改善できない」 という決意 目指した姿 「コト(Event ) 」 をデータとして扱う 業務が進捗するタイミングでデータが永続化される トリガーがあることが自然な状態にする 解決への転換点 ALL RIGHTS RESERVED BY 10X, INC. 15

Slide 16

Slide 16 text

シングルバスケットピックと総量ピッキング ピッキング方式の紹介 ALL RIGHTS RESERVED BY 10X, INC. 16

Slide 17

Slide 17 text

業務の進行 発生するイベント 業務の進行と永続化されるデータ ALL RIGHTS RESERVED BY 10X, INC. 17

Slide 18

Slide 18 text

「コト( イベント) 」中心の設計 アプリケーション上で変化が起きる操作には イベント が伴うものとして扱う 注文の商品単位でイベントを集約するルートを作成 ピック数のカウンターや制御を、この集約ルートで管理 集約ルートを整合性の境界とする 新しいデータ設計 ALL RIGHTS RESERVED BY 10X, INC. 18

Slide 19

Slide 19 text

リアルタイム性の向上 アプリ上で操作が起きたタイミングでクライアントとサーバが通信 データ設計の見直しにより、サーバレスポンスが高速化 これにより、都度通信しても業務に支障が出ないパフォーマンスを実現 以前の課題の解消 複数人での同時作業でも、 「コト」単位で処理されるため整合性が保たれる 競合の影響を最小限に抑えることができる 新しいシステム間のコミュニケーション設計 ALL RIGHTS RESERVED BY 10X, INC. 19

Slide 20

Slide 20 text

状態遷移には必ずイベントが伴うように! https://zenn.dev/jtechjapan_pub/articles/fc9878ec69b6a1 https://thinkbeforecoding.com/post/2021/12/17/functional-event-sourcing-decider プログラムの書きっぷりの変化 ALL RIGHTS RESERVED BY 10X, INC. 20

Slide 21

Slide 21 text

信頼できるデータ基盤 イベントを見れば、状態遷移に至る 過程 がわかる 分析チームに対して「常に信頼していいデータ」として提供可能に GA のログ欠損に怯える必要がなくなった 問い合わせ対応の改善 「まずは問い合わせ対象の操作と対になるイベントを見る」という動きが可能に 何が起きたかが明確になり、調査時間が短縮 改善の成果: 分析と信頼性 ALL RIGHTS RESERVED BY 10X, INC. 21

Slide 22

Slide 22 text

Firestore 設計の最適化 以前: 注文(Order )単位の巨大なドキュメント 改善後: 「注文に含まれる1 商品」単位のドキュメント 同一注文でも、別商品の操作であれば完全に並列処理が可能に 即時フィードバック スタッフの操作は速やかにサーバへ送信 「ピックしすぎ」などのエラーを即座にフィードバック可能に 改善の成果: 並列性と整合性 ALL RIGHTS RESERVED BY 10X, INC. 22

Slide 23

Slide 23 text

責務の分離 Picking: 注文の「1 商品」を扱う ピック操作や品切れ操作は商品単位で発生するため Packing: 「注文単位」で商品を扱う 全ての商品が揃ってから箱詰め・確定を行うため 非同期連携 Picking での操作(ピック)をトリガーに、Packing へ「パック予定」として計上 結果整合性を受け入れることで、Picking 操作時のレスポンスを高速に維持 注文単位で整合性を保つ必要があるPacking の操作でレスポンスをブロックしない 業務工程が分かれているため、即時の整合性は必須ではない Picking とPacking のモデル設計 ALL RIGHTS RESERVED BY 10X, INC. 23

Slide 24

Slide 24 text

ピックパック業務領域から見た役割の再定義 Order: ピックパック業務領域外への参照用のデータ(Read Model に近い) Picking/Packing モデル: ピックパック業務の更新系操作を主に受け持つ、新しい系統の読み込み用にも使う 結果整合性への移行 Picking/Packing モデルで業務の整合性を担保 発生した Event をトリガーに、Order へ状態を反映 Order への反映は遅延しても良い(結果整合性) Order との関係性 ALL RIGHTS RESERVED BY 10X, INC. 24

Slide 25

Slide 25 text

Phase 1: 同期実行 ユースケースからイベントハンドラを明示的に呼び出し 同一DB トランザクションで実行(まだ密結合) Phase 2: プログラム的非同期 イベントハンドラを非同期処理で実行 別のDB トランザクションに分離(レスポンス速度向上) Phase 3: システム的非同期 Eventarc を利用し非同期処理、内部的にはPub/Sub が使われる システム的に分離され、可能な限りのリトライが可能に Order への書き込み競合で失敗しても、ひたすらリトライして最終的に整合させる Order への反映: 3 つのフェーズ ALL RIGHTS RESERVED BY 10X, INC. 25

Slide 26

Slide 26 text

Future executeUseCase(Transaction tx, PickingId pickingId) async { // 1. リポジトリから対象の集約を取得 final picking = await repository.get(tx, pickingId); // 2. ドメインロジックの実行(状態遷移) final updated = picking.pick(); // 3. イベント、状態の保存 await repository.store(tx, updated); // 4. 発生したドメインイベントを順次処理 for (final evt in updated.occurredEvents) { await eventHandler.execute(evt, transaction: tx); } } Order への反映: (Phase 1: 同期実行) ALL RIGHTS RESERVED BY 10X, INC. 26

Slide 27

Slide 27 text

Future executeUseCase(Transaction tx, PickingId pickingId) async { // 1. 集約の取得 final picking = await repository.get(tx, pickingId); // 2. 状態遷移(ドメインイベントの発生) final updated = picking.pick(); // 3. イベント、状態の保存 await repository.store(tx, updated); // 4. プログラミング的な非同期処理 // 別トランザクション内でイベントを処理 for (final evt in updated.occurredEvents) { unawaited(() async { await eventHandler.execute(evt); }()); } } Order への反映: (Phase 2: プログラム的非同期) ALL RIGHTS RESERVED BY 10X, INC. 27

Slide 28

Slide 28 text

Future executeUseCase(Transaction tx, PickingId pickingId) async { final picking = await repository.get(tx, pickingId); final updated = picking.pick(); // 1. イベント、状態の保存 // 2. イベントはトリガーの仕組みで別のサーバへ送られる await repository.store(tx, updated); } /// 3. 別のサーバにてイベントを処理 Future receiveEvent(evt) { await eventHandler.execute(evt); } Order への反映: (Phase 3: システム的非同期) ALL RIGHTS RESERVED BY 10X, INC. 28

Slide 29

Slide 29 text

Eventarc + Firestore Firestore へのイベント書き込みをトリガーにEventarc が発火 購読者(Cloud Run )がイベントを処理してOrder 更新等の後続のロジックの実行 構成 ALL RIGHTS RESERVED BY 10X, INC. 29

Slide 30

Slide 30 text

以前の課題 「箱決定」のロジック内で、各配送システムへ直接連携メッセージを送信 連携先が増えるたびにロジック変更が必要(密結合) サーバダウン時にメッセージが送られないリスク 改善後 「箱決定イベント」 を連携コンポーネントが拾う形に変更 連携先の追加は、イベント購読者を追加するだけ(疎結合) イベントさえ永続化されていれば、リトライにより確実に連携実行(耐障害性向上) 導入効果事例: 配送連携ロジックの疎結合化 ALL RIGHTS RESERVED BY 10X, INC. 30

Slide 31

Slide 31 text

品切れ時の代替品登録業務 赤線の遷移がしれるようになった 知るためのデータが残っている 導入効果事例: 分析時のしなやかさ ALL RIGHTS RESERVED BY 10X, INC. 31

Slide 32

Slide 32 text

「時間」と「空間」の分離 ピックパック業務は日中がピークだが、夜間は停止する 連携先やOrder への反映が遅延しても、夜間に追いつけば問題ないケースが明確に 「何かあっても慌てなくていい」 運用が可能に メンテナンス性の向上 ピックパック領域のデータマイグレーションやインデックス作成 影響範囲が限定されたため、夜間であれば他業務(注文受付など)に影響を与えずに実施可能 導入効果事例: 疎結合が生んだ運用上のメリット ALL RIGHTS RESERVED BY 10X, INC. 32

Slide 33

Slide 33 text

機能要件 同一便( ≒同時間帯) の作業中に品切れがあれば「品切れあり」と表示 便が変われば(納品等の可能性があるため)再度確認を促す 「品切れなら見に行かなくていい」は現場のストレス軽減に重要 新機能への展開: 品切れ報告 ALL RIGHTS RESERVED BY 10X, INC. 33

Slide 34

Slide 34 text

実装のアプローチ CQRS 的なRead Model Updater イベントをトリガーに「品切れ状態」を集計・更新 画面表示時(RPC 呼び出し内)の集計は高コストなため回避 一貫したアーキテクチャ 表示のための集計が書き込み系ロジックに混在しない、パフォーマンスの足を引っ張らない 「この機能のために」場当たり的な実装をするのではなく、イベント駆動の仕組みに乗っかりながら実現 新機能への展開: 品切れ報告 ALL RIGHTS RESERVED BY 10X, INC. 34

Slide 35

Slide 35 text

良き点 業務はイベントを見出す前提で設計できるようになった ライト系はFirestore の特徴を活かせている どうにかしたい リード系のDB にFirestore のまま故にアプリケーションで集計ロジックを持たざるを得ない部分がある。 RDB なら「join してポンなのに!」という部分ばかり 良い点とどうにかしたい点 ALL RIGHTS RESERVED BY 10X, INC. 35

Slide 36

Slide 36 text

A. しないです。 既存システム全体を変えるのは非現実的 Q. 全体的にイベントを扱う設計とコマンドとクエリを分離する設計に移行します か? ALL RIGHTS RESERVED BY 10X, INC. 36

Slide 37

Slide 37 text

非同期処理の遅延との付き合い方 Read Model への反映/ 後続の処理が遅延することを前提に業務設計を行う必要があるが、どこまで遅延が許容され るのか? イベント発生から処理の完了までの時間を測定し監視している。 「イベント発生から処理の完了の計測」は内部ライブラリに沿っていると自動的に行われる。 難しいとこ ALL RIGHTS RESERVED BY 10X, INC. 37

Slide 38

Slide 38 text

難しかったこと 非RDB でRead Model (Order) を作る難しさ イベントハンドラでの集計ロジックが分厚くなりがち 非同期処理の運用 まとめ(1/2) ALL RIGHTS RESERVED BY 10X, INC. 38

Slide 39

Slide 39 text

反省点 集約ルートの設計: ピッキング中のちょっとした別業務もPicking に押し込んでしまった。 別の集約ルートに切り出さなかったゆえに、その後違和感が残ることに。 常に最適な境界を見つけるのは難しい(が、イベントがあれば後からでも分割できる) 得られたもの 「コト」をデータとして扱えるようになった 問い合わせや異常時の調査のしやすさ 分析基盤への信頼性の高いデータの提供 まとめ(2/2) ALL RIGHTS RESERVED BY 10X, INC. 39

Slide 40

Slide 40 text

業務の「コト(イベント) 」を扱う設計の重要性 事実が永続化されることで、分析や問い合わせ対応が容易に 業務の進捗が明確になり、信頼性の高いデータ基盤を構築可能に という効果もあるが! アプリケーションがコトを扱う設計になることで、 開発者が業務と目線を合わしながら設計をしやすくなる。と自分は思っている。 そしてそれはCQRS+ES に限らないよね。 今日伝えたいこと ALL RIGHTS RESERVED BY 10X, INC. 40